你在终端输入一句话并按下回车,随后屏幕开始逐字显示内容。其间,它还会自行执行几个命令、读取几份文件、修改一两个文件,最后交付一段总结。全程有时只需一两秒,有时则会持续几十秒。
这一秒之内究竟发生了什么?
模型是先一次想好全部内容再输出,还是一边思考一边行动?
那些"自行执行的命令"由谁决定,又是在何时作出的决定?
它们与最后输出的文字总结之间,又存在什么关系?
如果模型执行到一半改变决定——原本准备读取 A 文件,却又转而修改 B——这个过程是如何发生的?
要理解 Claude Code 这一类 Agent,这些问题正是关键入口。那么答案究竟是什么?
答案并不神秘:把完整过程展开,会看到一条常规流水线,再加上一个 while 循环。支撑模型实际动手的这套脚手架有一个名称:Harness。
系统先把你的输入加工为一条结构化消息,再将其传入一个名为 query 的异步生成器;
query 本身基本不处理事务,而是通过 yield* 将全部控制权转交给 queryLoop——也就是一个 while 循环。
在循环中,模型与工具轮流接手:模型提出"我要读这个文件",工具便执行并将结果放回;接着模型再次提出要求、工具再次运行,直至模型表示"我说完了"。
每个步骤都会借助 yield 将事件以流式方式传回 REPL,再由 REPL 使用 Ink 把内容逐字绘制到终端。
概括成一行:一次回合 = 一个 while 循环 + 一条流式事件管道。
这里需要记住两个关键词:其一是"循环",Agent 的全部"自主性"都由它产生;其二是"流式",无论打字机效果、工具实时进度,还是可随时中断的 Ctrl+C,都以 yield 这个字为基础。接下来的三节会逐层拆解这句话。
拆解它时,直接钻进源码并不是最省力的入口—src/query.ts 共有 1700+ 行,仅 queryLoop 便超过上千行,直接阅读很容易迷失。更合适的方式是先提取骨架:去掉 hook、权限校验、压缩、计费和日志等外壳后,它与一个 8 行玩具 Agent 在本质上没有区别。
以下内容就是 Harness 单次回合的最小骨架:
# 极简版:harness 行动回路的最小实现messages = [{"role": "user", "content": prompt}]while True:resp = client.messages.create(model=MODEL, messages=messages, tools=tools)messages.append({"role": "assistant", "content": resp.content})if resp.stop_reason != "tool_use": # 模型不再要工具 → 说完了breakresults = run_tools(resp.content)# 执行模型要的工具messages.append({"role": "user", "content": results})# 结果塞回去
盯住三个关键点:
while True:这个循环没有固定执行次数。结束时机不由计数器决定,而由模型自行表示"完了"。这正是 Agent "自主性"的来源;Harness 并未为这种"自主"编写任何一行,它完全存在于模型中。stop_reason:每轮运行时,模型都会提供一个停止理由。需要调用工具时就是 tool_use,完成表达时则是 end_turn。Agent 的所有行为分支,都依赖模型输出的这个字段。messages.append(...):工具结果并非在后台悄悄使用,而会作为一条新的 user 消息加入对话历史。模型始终看到一段连续对话,工具完成的"动手"由此被转换成对话中的"该你了"。记住这三点,后续进行Harness工程实践时,就不会被 hook、权限、压缩和子 Agent 等内容带偏;它们只是 Harness 为这副骨架增加的"器官",而骨架本身仅有这三行。
还要注意一个细节:这个极简版本采用"同步"方式——create 调用结束并得到完整 resp 后,流程才继续向下。Claude Code 的真实实现则采用流式方式,模型输出 token 的同时,循环也在接收。不过骨架没有改变,只是用一系列按时间到达的事件替代了"一次性 resp"。流式影响的是体验和响应速度,并非整体结构。
理解骨架后,再把 Claude Code 的实际实现叠加上去。下面的图呈现了一个完整回合的全景,也快速展示了 Harness 五脏在这一秒内的工作。每个节点都对应一个"器官",后文会分别放大说明;先记住这张图,因为它将引导后续全部内容。
下面依次查看各个节点,同时辨认这些器官:
handlePromptSubmit(src/screens/REPL.tsx),这一回合的"启动按钮"。Message,塞进对话历史。这一步会拼上 system prompt、CLAUDE.md、memory——那是上下文工程这个器官的活,后面详聊,这里只当成"造一条消息"。query(src/query.ts)。需要留意其声明中的 *——async function*,它是异步生成器,能够在运行过程中持续向外输出事件。query 自身几乎不执行工作,而是通过 yield* queryLoop(...)(src/query.ts)把全部控制权交给 queryLoop(src/query.ts)。行动回路真正的核心正是这里。stop_reason。tool_use 分支——先执行工具,再把结果写回消息,然后返回循环顶部继续询问模型。Agent之所以能"自己跑命令",源头正是这条回路;它也是工具系统(Part 2)接入行动回路的接口。end_turn 分支——完成收尾,并将整条路径中累积的事件持续 yield 出去。位于中间的 Stop Hook,是安全护栏介入流程的拦截点。query 的 yield* 回到 REPL 的 for await (const event of query({ ... }))(src/screens/REPL.tsx),onQueryEvent 把它翻译成 React state,Ink 把新 state 渲染到终端——这是人机交互的一端。用一句话归纳这张图:回车 → 加工 → query → queryLoop(模型与工具反复接力)→ 流式 yield → 渲染。生产级实现的全部奥妙都集中在中间的循环中,而这个循环就是行动回路。
现在重新审视第 1 节留下的疑问——"Harness 到底干了什么"。答案分布在图中的各个节点,并表现为三种可以直接观察到的现象:
yield。模型每生成一个 token,工具每输出一行,都会形成一帧事件;REPL 收到后便立即将其绘制到屏幕。所谓"打字机效果"并非前端动画,而是事件确实按照时序抵达,屏幕呈现速度也就是模型生成速度。正因为事件采用流式传输,按下 Ctrl+C 才能随时从某一帧中断。tool_use 分支。当模型判断"我需要读这个文件"时,queryLoop 便会运行相应工具,将输出放回对话历史,再返回循环顶部继续询问模型。你看到它"自己读了三个文件、改了一个",实际上是同一回路多次执行,每次模型都会依据最新结果重新作出决定。模型不会预先规划"读三个文件",而是在每一轮根据当前信息临时选择下一步。end_turn。只有模型主动返回这个 stop_reason,循环才会结束。在此之前,queryLoop 绝不会"自行决定结束";Agent 的终点始终由模型选择。可以发现,这三种现象没有任何一种源于 Harness 的"思考"。Harness 的职责只是把模型决策转换为动作,再将动作结果传回模型;智能始终位于模型一侧。
可以把整套 Harness 看成一个值班室。
你作为送件人,把一张工单(输入)送到窗口。值班员(queryLoop)接过工单后只负责一件事:致电专家(模型)并询问:"这个怎么处理?"
专家回答:"我需要查一下 X 文件。"值班员不会亲自查询,而是安排助理(工具)查找并带回结果,随后再次致电专家:"查到了,内容是这样,然后呢?"专家又回答:"那再看一下 Y。"流程就这样持续循环。
直到某一轮,专家表示:"行了,我懂了,答案是这个。"——end_turn。随后,值班员整理最终答案并从窗口交还给你。
关键在于理解分工:值班员与助理都属于 Harness,专家才是 Agent(模型)。值班员不负责思考,只传递信息和分派任务(行动回路);助理不作判断,只执行操作(工具系统);决策始终由专家完成。循环驱动且职责分明,这就是 Harness 的本质。Claude Code 的 query.ts 那 1700+ 行,大多用于给值班员、专家和助理增设护栏与助手:为值班员加入恢复机制(避免循环崩溃),替专家组织上下文(工作记忆),并给助理增加权限审批(护栏)。
还有一个常被忽略的细节:值班员与专家之间采用双工通话——专家说话的同时,值班员也在向外传递(流式 yield),并非等专家挂断后才统一汇报。这就是终端中文字逐个出现的原因。在这个比喻背后,对应的正是 TypeScript 中的 async function* 和 for await 这组语法:它们原本就是为"边生产边消费"的流式管道而设计。
回到开头的那个错觉:Claude Code 的智能并不藏在代码里。代码属于 Harness,Agent则是模型。所谓一次回合,不过是 Harness 利用一个 while 循环,将模型决策转换成动作,再以流式方式绘制到屏幕——query.ts 那 1700+ 行,都是围绕这个循环配置的器官与护栏。
而 Harness 的这些器官,也恰好构成整个系列的地图:
记住这份地图,它展示的就是整个Harness的五脏六腑。
现在重新审视一次会话的处理流程,会发现其中还存在一个问题:如果专家持续要求使用工具,始终不说"说完了",该怎么办?更现实的情况是,对话过长、上下文窗口接近耗尽、工具执行失败或权限遭拒时,循环如何自救?那个 8 行的 while True 完全 hold 不住这些情况,它要么会无限循环,要么会崩出一个 stack trace。
8 行的 demo 循环与 1700+ 行的生产级 queryLoop,真正的差异就在于"如何避免崩溃"。这也是 Harness 工程的一项核心难题:可靠性。
下一篇将深入 queryLoop 这个节点,查看单次循环中的 7 种 continue——其中每一行 continue 都代表一种自救操作:工具失败后如何重试、上下文将满时怎样压缩、配额超限时如何降级,以及模型越界后怎样拉回。只有理解这 7 条路径,才能真正相信 Agent 不会在途中卡死。
Let's go!