按下回车之后,Claude Code 在 100ms 里干了什么?

1. 字一个一个冒出来

你在终端里敲下一句话,按下回车。屏幕上字一个一个冒出来,中间还自己跑了几个命令、读了几份文件,改了一两个文件,最后给你一段总结。整个过程可能就一两秒,也可能几十秒。

那这一秒里,到底发生了什么?

  • 是模型一口气把所有东西都想好再吐出来,还是边想边做?

  • 那些"自己跑的命令"是谁、在什么时候决定跑的?

  • 它们和最后那段文字总结又是什么关系?

  • 如果模型中途改主意------本来要读 A 文件,读到一半又想去改 B------这是怎么发生的?

这些问题,恰恰是理解 Claude Code 这类 Agent 的钥匙。那答案是什么呢?

2. 答案是一条流水线

它的答案其实一点都不神秘:整个流程摊开看,是一条普通的流水线,加上一个 while 循环。这套让模型能动手的脚手架有个名字,叫 Harness

  • 你的输入先被加工成一条结构化消息,然后送进一个叫 query 的异步生成器;

  • query 自己几乎不干活,它通过 yield* 把控制权整个交给 queryLoop------一个 while 循环。

  • 循环里,模型和工具来回接力:模型说"我要读这个文件",工具就跑、把结果塞回去;模型再说、工具再跑,直到模型说"我说完了"。

  • 每一步都通过 yield 把事件流式喷回 REPL,REPL 再用 Ink 把它一个字一个字地画到你的终端上。

一行话:一次回合 = 一个 while 循环 + 一条流式事件管道

这里有两个关键词值得记住。一是"循环"------Agent 的所有"自主性"都来自它;二是"流式"------你看到的打字机效果、工具实时进度、可中断的 Ctrl+C,都建立在 yield 这个字上。下面三节,就是把这句话拆开揉碎来介绍。

3. 核心骨架就只有8行

要拆开它,最省力的入口不是直接啃源码---src/query.ts 有 1700+ 行,光 queryLoop 就占上千行,啃下去只会迷路。先把骨架剥出来。剥掉 hook、权限校验、压缩、计费、日志这些外壳,剩下的和一个 8 行的玩具 Agent 没有本质区别。

下面这段,就是 Harness 一次回合的最小骨架:

python 复制代码
# 极简版: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":   # 模型不再要工具 → 说完了
        break
    results = 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"换成了一串时序到达的事件。流式改变的是体验和响应度,不是结构。

4. Harness 五脏速览

有了骨架,我们把 Claude Code 真正的实现叠上去。下面这张图就是一次完整回合的全景------也是 Harness 五脏在这一秒里的速览。每个节点都是一个"器官",后面都会单独放大;但请先记住这张图,它是后面所有的导览。

逐个节点介绍一下,顺便认认器官:

  • ① 会话启动 :回车键触发 REPL 里的 handlePromptSubmitsrc/screens/REPL.tsx),这一回合的"启动按钮"。
  • ② 造一条消息 :裸文本被加工成结构化的 Message,塞进对话历史。这一步会拼上 system prompt、CLAUDE.md、memory------那是上下文工程这个器官的活,后面详聊,这里只当成"造一条消息"。
  • ③ 进入 query :消息交给 querysrc/query.ts)。注意它声明里的 *------async function*,异步生成器,可以一边跑、一边往外吐事件。
  • ④ 交给 queryLoopquery 自己几乎不干活,它通过 yield* queryLoop(...)src/query.ts)把控制权整个 交给 queryLoopsrc/query.ts)。这才是行动回路真正的心脏。
  • ⑤ 调用模型 :循环体里先调模型,拿到一个 stop_reason
  • ⑥ 执行工具tool_use 分支------执行工具、把结果塞回消息、回到循环顶继续问模型。这条回路就是 Agent "自己跑命令"的来源,它就是工具系统(Part 2)在行动回路里的接口。
  • ⑦ 收尾输出end_turn 分支------收尾、把整条路径上累积的事件一路 yield 出去。中间那道 Stop Hook,是安全护栏插进来的拦截点。
  • ⑧ 渲染到终端 :事件穿过 queryyield* 回到 REPL 的 for await (const event of query({ ... }))src/screens/REPL.tsx),onQueryEvent 把它翻译成 React state,Ink 把新 state 渲染到终端------这是人机交互的一端。

一句话总结这张图:回车 → 加工 → queryqueryLoop(模型/工具来回接力)→ 流式 yield → 渲染。生产级的所有魔法,都缩在中间那个循环里;而那个循环,就是行动回路。

5. 回到最初的悬念

现在回头看第 1 节留下的那个悬念------"Harness 到底干了什么"。答案就散在这张图的节点上,化作你能看见的三种现象:

  • "字一个一个冒出来" = 流式 yield。模型每生成一个 token、工具每产出一行,都是一帧事件,被 REPL 收到就立刻画到屏幕。你看到的"打字机效果"不是前端动画,是事件真的按时序到达------屏幕显示的速度,就是模型生成的速度。也正因事件是流式的,你按 Ctrl+C 才能随时在某一帧打断它。
  • "自己执行命令" = tool_use 分支。模型判断"我需要读这个文件",queryLoop 就让对应工具跑、把输出塞回对话历史,再回到循环顶继续问模型。你看到它"自己读了三个文件、改了一个",其实是这条回路被走了多次,每次都让模型基于新结果再决策一次。模型并不会预先规划"读三个文件"------它每一轮根据最新信息临时决定下一步。
  • "说完了" = end_turn。模型主动给出这个 stop_reason,循环才会退出。在此之前,queryLoop 永远不会"自己决定结束"------Agent 的终点,始终是模型自己选的。

发现了吗?这三种现象里,没有一种是 Harness "想"出来的。 Harness 只负责把模型的决策搬运成动作、把动作的结果搬运回模型。智能始终在模型那一边。

6. 把 Harness 想成一个值班室

把整套 Harness 想成一个值班室。

你是送件人,把一张工单(输入)递进窗口。值班员(queryLoop)拿到工单后只做一件事:打电话问专家(模型):"这个怎么处理?"

专家回:"我需要查一下 X 文件。"值班员不自己查,而是让助理(工具)去查、把结果带回来,再打电话给专家:"查到了,内容是这样,然后呢?"专家又回:"那再看一下 Y。"如此循环。

直到某一次,专家说:"行了,我懂了,答案是这个。"------end_turn。值班员把最终答案整理好,从窗口递回给你。

关键看分工:值班员和助理都是 Harness,专家才是 Agent(模型)。 值班员不思考,只传话和派活(行动回路);助理不判断,只动手(工具系统);真正做决策的只有专家。循环驱动、分工清晰------这就是 Harness 的本质。Claude Code 的 query.ts 那 1700+ 行,大部分都是在给值班员、专家、助理各自添置护栏和助手:给值班员加恢复机制(不让循环崩)、给专家拼好上下文(工作记忆)、给助理加权限审批(护栏)。

还有个容易忽略的细节:值班员和专家之间的电话是双工 的------专家一边说,值班员一边往外递(流式 yield),不是等专家挂电话才一次性汇报。这正是你在终端看到字一个一个冒出来的原因。这个比喻背后,正好对应 TypeScript 里 async function*for await 这对语法:它们天生就是为"边产边消费"的流式管道设计的。

7. 智能不在代码里

回到最开始那个错觉:Claude Code 的智能,不在它的代码里。代码是 Harness, Agent是模型 。一次回合,不过是 Harness 用一个 while 循环把模型的决策变成动作、再流式画到屏幕------query.ts 那 1700+ 行,全是给这个循环配的器官和护栏。

而这套 Harness 的器官,正好就是整部系列的地图:

  • 行动回路------让模型动起来,且不让它崩(这是 Harness 的心脏);
  • 工具系统------给模型的手;
  • 上下文工程------给模型的工作记忆;
  • 安全护栏------让模型放手干又不闯祸;
  • 多 Agent 编排------从单兵放大到团队;
  • 可扩展与人机交互------对接世界,也接住人的插手。

记住这张地图,这个就是整个Harness的五脏六腑。

8. 后记

现在我们回顾一次会话的处理过程,会发现里面藏着一个问题:万一专家一直要工具、永远不说"说完了"怎么办?或者更现实的------对话太长、上下文窗口快爆了、工具失败了、权限拒绝了,这时循环怎么自救?那 8 行的 while True 根本 hold 不住,它会要么无限转下去、要么崩出一个 stack trace。

8 行的 demo 循环,和 1700+ 行的生产级 queryLoop,差就差在"怎么不崩"上。 这正是 Harness 工程的核心难题之一: 可靠性

下一篇,我们就钻进 queryLoop 这个节点,看它一个循环里的 7 种 continue ------每一行 continue 都是一种自救动作:工具失败时怎么重试、上下文将满时怎么压缩、配额超限时怎么降级、模型越界时怎么拉回来。把那 7 条路看明白,你才会真正相信:Agent 不会在半路卡死。

Let's go!

相关推荐
leeyi2 小时前
ReAct Agent 源码拆解:Eino 如何把 Graph 变成 Agent(第64篇-E50)
llm·aigc·agent
Loocor2 小时前
CLI、MCP、Skills 同时在场 —— 从 8 个 Agent 源码看到的
agent
思考着亮2 小时前
10.记忆 (Memory)
agent
Raistwen2 小时前
Agent架构师从0出发(第1篇):Python环境与依赖管理的N个致命陷阱 —— 一个Java架构师的AI入门血泪史
agent
冻感糕人~3 小时前
大模型学习指南:收藏这份AI Agent四层工程地图(小白程序员必备)
java·大数据·人工智能·学习·大模型·agent·大模型学习
AIGC安琪6 小时前
同样接入大模型,如何抉择 Workflow 与 Agent?
人工智能·ai·语言模型·大模型·agent·workflow·大模型学习
深念Y6 小时前
AI编程Agent工具定义对比分析
agent·ai编程·开源项目·工具·tool·hermes·ccsiwtch
tkevinjd14 小时前
MiniCode 项目详解6:原项目控制系统的10个缺陷(已修复)
python·llm·agent