用 Claude Code 或者 Codex 的时候,你只告诉它「实现这个需求」或者「帮我修这个 bug」。
后面的事,它会自己去翻项目、找关联文件、规划改动、跑测试、看结果。
你没有一步步告诉它先读哪个文件、再跑哪个命令,它也能自己往下推进。
它是怎么把这些事一路接下去的?LLM 不是只会一问一答吗?
最近很火的 Harness Engineering,讲的就是这层运行时骨架。
网页 Chat 和 Claude Code、Codex 这类 Agent 工具,外面都有 Harness。
只是一般的 AI 对话功能,这层很薄,最薄的时候只是整理 messages、调一次模型、把回答流式刷回来,一圈就停。
Agent 工具这层会厚很多。
它会让模型继续读文件、调工具、拿结果,再回去判断下一步。
Harness 跑得越深,模型能自己接下去的事情就越多。
先划重点
你用的网页 Chat、Claude Code、Codex,外面都套着一层 Harness。
差别只在它搭到多深——Chat 一圈就停,Agent 会一直往下跑。
骨架就是一个循环,每圈五件事:整理输入 → 调 LLM → 判断要不要调工具 → 跑工具 → 拼回下一圈。
从 demo 到能交付的 AI 应用,中间是Harness 的六块工程能力:
主循环、工具注册、上下文管理、权限安全、多 Agent 编排、持久化与记忆。
Model 只负责一次调用,Agent 靠 Harness 往下跑
可以先把 Model 想成一个「大脑」。
它理解能力很强,推理也快,也能判断下一步想做什么。
比如它可以在回答里说:「我想读这个文件」「我想调这个工具」。
但 Model 只会输出这个意图,它自己不会真的去读文件、执行命令、拿结果,也不会把结果塞回下一轮继续跑。
这就像一个很聪明的人刚到公司,他知道接下来应该查代码、跑测试、看日志。
但电脑还没发,账号没开,工具没配,系统权限没给,流程和交接文档也没有。
他想做这件事,也没有地方下手。
Harness 补的就是这套工作环境和调度系统。
要看懂 Agent 是怎么工作的,可以先把这套东西拆成三块:
Model、Harness、Tool。
Model 负责决定下一步想做什么。它会输出一段 tool_use,意思是「我想调哪个工具,参数是什么」。
Harness 负责解析这段 tool_use。它检查这一步能不能做,交给工具执行,拿到结果,再把结果放回下一轮上下文。
Tool 才是具体能力,比如 Read、Bash、Grep。读文件、跑命令、搜索代码,都是 Tool 真正在做。
所以我会把 Agent = Model + Harness 先理解成这件事:
光有大脑不够,还得有一套能让它动起来的工作环境。
如果只是一问一答,骨架其实很短:
// 普通调用:回一条结果,这一轮就结束
const response = await llm.call(messages);
return response;
到了 Agent,Harness 会解析 tool_use,执行对应工具,再把结果送回下一轮:
while (true) {
// 先调一次 LLM,看它这一轮想做什么
const response = await llm.call(messages);
/*
* response 里可能同时有 text 和 tool_use。
* text 是给用户看的文字,tool_use 是模型说「我要调工具」。
* 这里直接从 content 里找 tool_use。
*/
const toolUses = response.content.filter(
block => block.type === 'tool_use'
);
// 没有 tool_use,说明这一轮只是回答,任务到这里结束
if (toolUses.length === 0) {
return response;
}
// 有 tool_use,就执行工具,把 tool_result 写回 messages,再进入下一圈
const results = await executeTools(toolUses);
messages.push(...results);
}
两段代码真正不一样的地方,就在 toolUses.length === 0 这个判断。
没有 tool_use,这轮就直接返回。
有 tool_use,Harness 先执行工具,把 tool_result 写回 messages,再让它进下一圈。
Agent 往下做事时,会反复走这五步
前面那段 while(true) 只是骨架。只要模型还需要调工具,Harness 就会按这个顺序继续往下走。
Claude Code 的实现里,这五步就是主循环的骨架,我们逐步对照,看每一层是怎么做的。
上下文治理 — 把喂给 LLM 的 messages 整理干净
调 LLM 之前,Harness 会先整理这一轮要喂进去的 messages,避免上下文撑爆。
太长就想办法缩短,要么把前面的旧对话摘要掉,要么把某条工具结果截断。
LLM 每次只能看一个有限的窗口,这一层要保证喂进去的东西塞得下。
那 Claude Code 里面是怎么处理的?
/**
* 只取最近这段
*/
let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]
/**
* 工具结果太长就截断
*/
messagesForQuery = await applyToolResultBudget(...)
/**
* 合并重复的工具结果
*/
const microcompactResult = await deps.microcompact(...)
/**
* 接近上限才触发 LLM 压缩
*/
const { compactionResult } = await deps.autocompact(...)
取最近这段消息,截断太长的工具结果,再把重复读取的内容合并掉。
能用规则处理的先用规则处理,真的快装不下了,再用 LLM 做压缩 ——
因为上下文压缩可是会烧掉不少 token的。
调 LLM — 让大脑说一次话
整理完 messages,Harness 会把 messages 和工具清单一起发给 LLM。
这一轮模型会返回两类东西:
给用户看的文字,以及可能出现的 tool_use。
tool_use 其实就是模型的下一步意图:
它想调哪个工具,参数是什么。
const response = await llm.call(messages, tools);
// response.content 里可能同时有 text 和 tool_use
response.content = [
{ type: 'text', text: '我来帮你读文件' },
{
type: 'tool_use',
name: 'Read',
id: 'toolu_xxx',
input: { file_path: 'src/api.ts' }
}
];
工具意图是 LLM 自己在回答里带出来的,Harness 只管执行——谁决策、谁执行,分得很清楚。
判断 tool_use — 它想调工具,还是想直接答你
response.content 里有没有 tool_use,决定这一圈怎么走。
没有,模型直接回答了,循环退出。
有,进下一步执行工具。
用户给的是任务描述,模型自己判断这一轮要不要调工具。
你问一个能直接回答的问题,它不需要工具,这轮就结束;
但如果你的问题涉及最新的时效性数据,它可能就会先调搜索工具。
你让 Claude Code 修 bug,它会连续请求读文件、查日志、改代码、跑测试。
任务越复杂,模型请求工具的次数越多,Harness 要跑的轮次也就越多。
执行工具 — 真的去跑那个工具,拿到结果
在执行工具这一步,Harness 要做三件事:
检查能不能执行、调用真正的 Tool、收集工具结果。
比如 Read('src/api.ts'),Harness 需要先确认这个路径能不能读、有没有权限,检查通过后才能执行这个读取文件的工具。
for (const call of toolUses) {
/**
* 询问用户是否允许执行这个工具
*/
await checkPermission(call);
const result = await runTool(call);
toolResults.push({
type: 'tool_result',
/**
* 和 tool_use 的 id 配对
*/
tool_use_id: call.id,
content: result
});
}
这一轮所有工具都跑完以后,Harness 才会进入下一步,开始组装下一圈 messages。
组装下一圈 — 把结果拼回 messages,回第一层
回到下一轮之前,Harness 要把三份东西合并好:
之前的 messages 历史、这一轮 LLM 带着 tool_use 的回答、这一轮所有工具结果。
拼好之后 continue,回到第一步的上下文治理,再开始下一圈循环。
state = {
messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
turnCount: state.turnCount + 1
};
LLM 下一圈看到的是:
我说过要调 Read 工具,Read 返回了这些内容,现在轮到我决定下一步。
messages 数组就是 LLM 的记忆,每圈拼一次,循环才能一直接下去。
到这里,再回头看这五步的顺序,就很清楚了:
上下文治理在前:先把 messages 整干净,再喂给模型。
工具执行在后:先等模型返回 tool_use,Harness 才知道该不该跑工具、跑哪个、带什么参数。
简单问题一轮结束,复杂任务多轮推进
同样是这个循环:
任务越简单,停得越早。
你问它一个概念问题,比如「这个错误大概是什么意思」,模型直接回文字,没有 tool_use,一圈就结束。
你让它看一个具体文件,比如「帮我看看这个接口为什么返回空数组」,它可能先 Read 文件,再结合结果回答,通常多跑一两圈。
你让 Claude Code 修一个真实 bug,就不一样了。
它会先读报错,再 Grep 相关代码,打开几个文件,改一版,跑测试;测试失败,再读失败日志,继续改。
这时候循环跑的就不是一两圈了。
骨架其实还是那五步,差别就在于:
任务越复杂,模型越需要工具结果,Harness 就要陪它跑更多轮。
Harness 的六块能力,都是循环跑深以后冒出来的问题
前面那五步,讲的是 Agent 一轮一轮怎么跑。
但 Harness 不只是一段 while(true)。
那段循环只是运行路径。要让它从 demo 变成能交付的 AI 应用,问题很快就会冒出来:
模型能调哪些工具?
每个工具的参数长什么样?
上下文快爆了怎么办?
Bash 这种执行 shell 命令的高风险工具,什么时候必须让用户确认?
一个任务太大,能不能拆给子 Agent 并发跑?
跑到一半挂了,重启后能不能接着做?
这些问题合在一起,才是 Harness 真正吃重的地方。
拆开看,就是六块能力:
主循环:让模型不止回答一次,而是拿结果继续判断。
工具注册:告诉模型能用哪些工具、每个工具怎么传参。
上下文管理:控制 messages 的长度和质量,别让它撑爆。
权限安全:在真正执行工具前,拦住危险操作。
多 Agent 编排:任务太大时,把一部分工作交给子 Agent。
持久化与记忆:让状态和偏好跨轮、跨会话保留下来。
主循环 — 让模型不只回答一次
一次模型调用结束后,还能继续读结果、再判断下一步,靠的是外面那个 while(true) 主循环。
没有这层循环,一次模型调用结束,这轮任务也就结束了。
加上这层循环,事情就能继续往下走:
看一次结果,判断一次下一步,再决定要不要继续调工具。
放到 Claude Code 的主流程里看,跑循环的核心函数叫 query。
它里面大概有三个出口:
/**
* 正常结束
*/
if (!needsFollowUp) return { reason: 'completed' };
/**
* 轮数兜底
*/
if (nextTurnCount > maxTurns) return { reason: 'max_turns' };
/**
* 用户中断
*/
if (abortSignal.aborted) return { reason: 'aborted_tools' };
这三个出口里,最常走的是第一条。
needsFollowUp 为 false,说明这一轮没有 tool_use:
模型已经把话说完了,后面没有工具要跑,循环就停。
如果这一轮带了 tool_use,那就还没完:
Harness 会去跑工具,把结果放回 messages,再让模型判断下一步。
而轮数兜底和用户中断其实就是两道保险:
一个防止它在任务里无限绕下去,一个让用户随时能刹车。
工具注册 — 让模型知道自己能用什么
模型不会凭空知道系统里有哪些工具。
工具注册解决的就是这件事:调用模型之前,先准备一份工具清单。
在 Claude Code 里,Read、Bash、Grep 这些内置工具,是代码里先定义好的。
用户接入 MCP 以后,外部工具也会被包装成同一类工具对象。
实际发请求前,这份清单会从两路拼出来:产品内置工具,加上用户接入的 MCP 外部工具。
拼好以后,它会跟 messages 一起放进 LLM 请求里。
模型看到这份清单,才知道这一轮能使用哪些工具。
而这份工具清单,至少要说清三件事:工具叫什么、什么时候该用、参数怎么填:
const response = await llm.call(messages, [
{ name: 'Read', description: '读文件', inputSchema: {...} },
{ name: 'Bash', description: '执行 shell', inputSchema: {...} },
{ name: 'Grep', description: '搜索代码', inputSchema: {...} }
])
所以 tool_use 是模型看完工具清单以后,返回的一次工具请求。
如果模型返回了清单外的工具名,主流程找不到对应工具,也就不会执行。
上下文管理 — 别让 messages 撑爆
每次调用模型前,messages 都要先过一遍。
上下文窗口再大,也不是无限的。
Agent 连续读文件、跑命令、拿日志,messages 会涨得很快。
一旦上下文又长又乱,模型就容易降智:
漏看上下文、重复绕路、把已经确认过的事又翻出来怀疑一遍。
在实际实现中,一般不会一上来就让 LLM 总结整段上下文历史。
先做便宜的处理:该截断的截断,该合并的合并,真的快装不下了,再压缩。
拆开看,其实就是三类动作:
预算分配:system prompt、工具结果、历史消息,各自能占多少。
规则整理:截断太长的工具结果,合并重复读取的内容。
自动压缩:接近上限时,让 LLM 生成摘要,替换掉一部分老历史。
关于上下文压缩的思路,感兴趣的朋友可以看看上一期的文章:link。
权限安全 — 敏感操作要先确认
工具真正执行前,还要过一道权限判断。
只看工具名不够。
同样是执行 shell 命令,ls -la 只是看看目录,rm -rf / 就是高风险操作。
同样是读文件,一个是当前工作区里的 src/api.ts,一个是系统路径里的 /etc/passwd,权限判断也会不一样。
所以权限判断要看两样东西:工具名 + 这次传进来的参数。
放到代码里,可以先把它想成一个 canUseTool(tool, input)。
tool 说明它想用哪个能力,input 说明这次具体要做什么。
async function canUseTool(tool, input) {
if (tool.name === 'Bash' && isDangerous(input.command)) {
// 敏感操作先停下来,让用户确认
return await askUser();
}
// 安全操作直接放行
return 'allow';
}
在 Claude Code 里,每次执行敏感工具前,都会先停下来问用户要不要继续。
用户没确认之前,这次工具调用就不会往下跑。
多 Agent 编排 — 把一部分工作交给子 Agent
任务一大,全放进一个 Agent 跑,上下文就会越来越乱。
主目标、读过的文件、跑过的命令、还没处理完的反馈,全混在一起。
OpenAI 在他们的 Harness Engineering 博客里讲过一个 Codex 的工作流,是这么解决的:
Codex 先写代码,开 PR,然后请几个 agent 分别评审,每个只看一件事:代码质量、测试覆盖、安全问题、规范检查。
评审意见回来,Codex 继续改,改完再送审,一轮不过再来一轮,直到所有 reviewer 都点头。
OpenAI 把这套流程叫 Ralph Wiggum Loop。
Ralph Wiggum 是《辛普森一家》里那个一直说 I'm learnding! 的呆萌小孩,有点懵,有点笨拙,但很认真地在学。
拿他来命名,我觉得是一个很精准的调侃:
很多看起来很高级的多 Agent 编排,拆开以后,本质上其实就是一个老老实实不断迭代的循环。
别期待第一轮就对,先跑一轮,拿反馈,再改一轮。
持久化与记忆 — 任务能接上,记忆跨会话
持久化解决的是中断后能否续上。
崩溃了或者主动退出,历史都在,用 /resume 重新打开能从断的地方接着跑。
记忆解决的是跨会话失忆。
今天你让它记住「我项目用 pnpm 不用 npm」,明天新开一个会话,它还记得。
Claude Code 把记忆分两块管——
CLAUDE.md 是固定记忆,每圈都带着;
~/.claude/memory/ 是自动记忆,按相关性筛选后注入。
// CLAUDE.md 内容进 system prompt,每次调用都带着
const systemPrompt = buildSystemPrompt({ claudeMd });
// 记忆预取放在循环外,只读一次,不随每圈重复触发
const pendingMemoryPrefetch = startRelevantMemoryPrefetch(userMessage);
while (true) {
// 按相关性筛选后注入 messages,每次用户消息只注入一次
messages = await injectMemoryOnce(pendingMemoryPrefetch, messages);
const response = await llm.call(messages, tools);
// ...
}
这条链路,就是 Harness 在做的事
Claude Code 真实的主循环实现有上千行,大部分在处理各种边界——
中断、超时、记忆预取、多 Agent 分流、MCP 工具接入。
但背后机制其实就是:
Model 负责推理,Harness 负责其他所有事:把上下文喂进去、决定调哪个工具、管权限、拼下一圈。
Claude Code、Codex 这类工具,用起来像是它在自己做决定。
拆开来,就是一个 while(true) 加一个 if(tool_use)。
差别只在 Harness 搭到多深。
读完回顾
Q1:Claude Code 修一个 bug,可以自己翻文件、改代码、跑测试,不用你每步都发消息推它——为什么?
💡 提示:回到 Model + Harness 公式
Q2:你做的 AI 产品接了 PDF 问答,用户反馈「对话跑十几轮之后 AI 开始胡说八道」,你会先去查 Harness 六块能力里的哪一块?
💡 提示:六块能力各自负责什么,哪块和「对话越长、质量越差」最直接相关
Q3:Harness 六块能力里,哪一块去掉之后 Agent 跑起来最危险?为什么?
💡 提示:想想 Agent 能执行的操作,以及出错了能不能撤回
延伸阅读
-
Harness engineering: leveraging Codex in an agent-first world | OpenAI
-
Harness design for long-running application development | Anthropic
如果这篇帮你理清了 Agent 运行时的骨架,下一篇我们往这条链路里填入第一张拼图,看看 Agent 的知识获取到底怎么做。
感兴趣可以关注微信公众号前端Fusion,不错过后续更新。