核心循环:Agentic Loop
大约 4 分钟
核心循环:Agentic Loop
最简循环:50 行代码的本质
剥掉所有工程细节,Claude Code / Codex 的核心就是这个循环:
messages = [system_prompt, user_task]
while True:
response = llm(messages) # ① 调模型
if response.has_tool_calls: # ② 模型想用工具
for call in response.tool_calls:
result = execute(call) # ③ harness 执行
messages.append(result) # ④ 结果回注
continue # ⑤ 回到 ①
break # 模型给出最终回答,循环结束
全部魔法就在这:模型没有能力「循环」,是 harness 替它循环。模型每轮只是看到(之前的对话 + 工具结果),然后决定下一步说什么。
循环的每一环:工程细节
① 系统提示:循环的「宪法」
系统提示在每轮都置顶,定义:
- 你是谁、当前工作目录、OS、日期
- 有哪些工具、怎么用
- 行为准则(「写代码要匹配项目风格」「破坏性操作先确认」)
- 环境注入的信息(git 状态、CLAUDE.md 记忆文件内容)
系统提示是 harness 里最重的「隐性工程」。Claude Code 的系统提示有数千 token,精确到「引用代码时用 file:line 格式」这种细节。
② 工具调用协议:模型如何「行动」
现代 API 的 tool use 是原生能力,不是文本解析:
// 请求时声明工具(JSON Schema)
{
"tools": [{
"name": "Read",
"description": "读取文件内容",
"input_schema": {
"type": "object",
"properties": { "file_path": { "type": "string" } },
"required": ["file_path"]
}
}]
}
// 模型响应(停止原因 = tool_use)
{
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "我先看看配置文件" },
{ "type": "tool_use", "id": "t1", "name": "Read",
"input": { "file_path": "package.json" } }
]
}
历史彩蛋:没有原生 tool use 的年代,LangChain 的 ReAct 靠提示词让模型输出
Action: Search[query]文本再正则解析——这就是为什么老教程都是这个画风。
③ 执行:harness 的「手」
def execute(call):
if not permission_check(call): # 权限闸门(见 08 篇)
return error_result("用户拒绝了此操作")
try:
return dispatch_to_handler(call) # 每个工具有独立 handler
except Exception as e:
return error_result(str(e)) # 错误也作为结果回注!
关键设计:工具报错不抛异常打断循环,而是把错误文本喂回给模型,让它自己决定重试、换方案还是求助用户。这是 agent「自愈能力」的来源。
④ 结果回注:观察决定下一步
// 下一轮请求 messages 尾部追加
{ "role": "user", "content": [
{ "type": "tool_result", "tool_use_id": "t1",
"content": "{ \"name\": \"demo\", ... }" }
]}
模型于是「看到」文件内容。注意 tool_result 的 role 是 user——在 API 层面,工具结果是作为用户侧消息注入的。
⑤ 终止条件
正常终止:模型不再请求工具,输出最终回答(stop_reason = end_turn)
强制终止:达到最大轮数 / token 预算 / 用户按 Esc 中断
异常终止:API 超时、模型输出非法 schema(harness 应重试并把错误告知模型)
一次真实任务的循环轨迹
用户:「修复这个测试」
轮 1 模型 → Grep("test.*login") 找到 tests/login.test.ts
轮 2 模型 → Read("tests/login.test.ts") 看到断言
轮 3 模型 → Read("src/login.ts") 发现源码逻辑
轮 4 模型 → Edit("src/login.ts", ...) 修改
轮 5 模型 → Bash("npm test") 测试失败(错误回注)
轮 6 模型 → Edit(...) 再改 根据报错修正
轮 7 模型 → Bash("npm test") 通过
轮 8 模型 → 文本回答「已修复,原因是...」 end_turn,循环结束
注意轮 5-6:失败的工具结果是最好的反馈信号。好的 harness 不怕报错,怕的是「命令成功了但输出没意义」(比如管道吞了 stderr)。
流式与中断
真实产品的循环不是一问一答的傻等:
- 模型输出流式渲染:用户实时看到模型在「想什么、做什么」
- 中断不丢弃:Esc 打断后,已完成的工具调用结果保留在上下文中,
用户可以补充指令继续——而不是从零重来
- 可中断的粒度在「轮」之间:当前轮的工具执行完才停(保证状态一致)
循环的失控模式与刹车
| 失控模式 | 表现 | harness 的刹车 |
|---|---|---|
| 无限循环 | 反复 Read 同一文件、来回改代码 | 重复检测 + 最大轮数限制 |
| 工具风暴 | 一轮并发请求过多工具 | 单轮工具数上限 |
| 幻觉工具 | 调用不存在的工具/参数 | schema 校验,错误回注让它自纠 |
| 越跑越偏 | 上下文污染后行为异常 | 上下文压缩 / 开新会话(见下篇) |
并行工具调用
现代模型支持一轮返回多个 tool_use:
轮 1 模型同时请求: Read(a.ts)、Read(b.ts)、Grep("import")
harness 并发执行 3 个(只读工具天然可并行)
3 个结果一起回注 → 轮 2
收益:读密集型任务快 3-5 倍
原则:只读并行、写串行(避免并发写冲突)
本篇小结
- Agentic loop = 「调模型 → 执行工具 → 结果回注」的 while 循环
- 模型没有自主性,循环体在 harness 里
- 错误当数据回注,是 agent 自我修复能力的基础
- 实际产品 = 这个循环 + 流式渲染 + 中断保持 + 失控刹车
下一篇:上下文工程——循环每转一圈,上下文都在膨胀,这是 harness 最难的问题。
