一次Agent调用背后发生了什么
第一件事:它得决定先做什么。
这个"想一步、做一步、看一步"的循环,就是 Agent Loop。它是 Agent 的心跳——没有这个循环,AI 就只是一个问答机器。
第二件事:它得能读文件、跑命令。
光"想"没用,得有手。Claude Code 需要调用 Read 工具读文件、调用 Bash 工具跑 pnpm add dayjs、调用 Edit 工具改代码。
这些工具的定义、调度、权限管理、并发控制,组成了 Tool System。它是 Agent 的手脚——没有工具,Agent 就是个只会说不会做的嘴炮。
第三件事:它得记住前面做了什么。
这就是 Context Engineering,上下文工程。它是 Agent 的大脑供血系统——决定 Agent 能"看到"多少、"记住"多少。
第四件事:它得记住你是谁。
这是 Memory 系统——跨会话的长期记忆。和 Context Engineering 不同,Context 管的是单次会话内的信息,Memory 管的是跨会话的持久化。
第五件事:复杂任务,一个 Agent 搞不定。
这就是 Multi-Agent——不是为了"分角色"搞什么 PM + 设计师 + 开发者的 cosplay,而是为了分隔上下文。
第六件事:权限、Hook、重试、优雅退出……
这些"包裹在模型外面"的工程系统,就是 Harness Engineering。它是 Agent 的骨架——模型再强,没有一个好的骨架,也跑不起来。
六大支柱
这一次看似简单的工具调用,背后牵扯了六个大的系统:

支柱 | 一句话 | 人体类比 |
|---|---|---|
Agent Loop | 想一步、做一步、看一步的循环 | 心跳 |
Tool System | 读文件、跑命令、调 API | 手脚 |
Context Engineering | 管理有限的上下文窗口 | 大脑供血 |
Memory | 跨会话的长期记忆 | 长期记忆 |
Multi-Agent | 拆任务、分上下文 | 团队协作 |
Harness Engineering | 权限、重试、Hook、生命周期 | 骨架 |
记住这六个词。 后面不管出什么新的 Agent 产品、新的框架、新的论文,你都可以用这六个维度去拆解它。它在 Agent Loop 上做了什么创新?Context Engineering 怎么处理的?工具系统是什么设计?
这六个支柱不是平行的,它们之间有依赖关系。我建议的学习路径是从内到外:

Agent Loop(心跳):先理解 Agent 的最小可运行单元
Tool System(手脚):Agent 能"做事"了,你才能观察后面的问题
Context Engineering(供血):Agent 跑起来之后,上下文爆了怎么办?这是深水区的入口。
Memory(记忆):单次会话、跨会话,上下文记忆怎么来管理?
Multi-Agent(协作):一个 Agent 不够用,怎么拆成多个?
Harness Engineering(骨架):上面都搞定了,怎么包成一个生产级系统?
while循环改变了什么?
Agent的最小模型:while(true)
如果让你用代码来表达 Agent 的核心逻辑,最简单的版本长这样:
TYPESCRIPT
while (true) {
const response = await llm.chat(messages) // 想:让模型决定下一步
if (response.toolCalls.length === 0) {
break // 模型认为任务完成了,没有工具要调
}
for (const toolCall of response.toolCalls) {
const result = await executeTool(toolCall) // 做:执行工具
messages.push(result) // 看:把结果加入上下文
}
}简单版是"想-做-看"三步,Claude Code 的实际流程是这样的:

第一步:准备上下文
在调 API 之前,先检查上下文是不是快爆了。如果快到上限了,触发压缩——先试轻量级的 Snip(删掉老消息),不行就 Microcompact(局部摘要),再不行就 Auto-compact(全局摘要)。
第二步:调模型 API
哪些操作可以并发、哪些必须串行——是 Agent 工具系统里一个很重要的设计点。
第三步:决定是否继续
Claude Code 有 7 种退出路径:
退出原因 | 什么时候触发 |
|---|---|
| 模型没有调用工具,认为任务完成了 |
| 流式传输过程中被中断 |
| 工具执行过程中被中断 |
| Hook 阻止了继续执行 |
| 超过了最大轮次限制 |
| 上下文太长,API 拒绝了 |
| 压缩后还是太长,无法恢复 |
第四步:执行工具,收集结果
第五步:处理附加任务
Prompt被拆成了什么?
简单说,模型不认识"文字",它只认识数字。所以你输入的任何内容——中文、英文、代码、标点符号——都会先被切成一小块一小块的"token",每个 token 对应一个数字编号。

大部分大模型用的分词算法叫 BPE(Byte Pair Encoding)。简单理解就是:模型在训练之前,先统计大量文本中哪些字符组合出现频率最高,把高频组合合并成一个 token。英文训练数据比中文多得多,所以英文的合并更充分,一个词一个 token;中文经常要拆开来编码。
这直接导致了一个实际问题:同样的语义内容,中文比英文消耗更多的 token。
好,现在你的 prompt 已经变成了一串 token 数字,喂给模型了。接下来模型要开始生成回复。大模型不是"想好了再说"的,它是"边说边想"的。什么意思呢?模型生成文本的过程,是一个 token 接一个 token 往后蹦的。它先生成第 1 个 token,然后把第 1 个 token 加入输入,再生成第 2 个 token,然后把前 2 个 token 加入输入,再生成第 3 个……如此循环,直到生成一个"结束"标记。这个过程叫自回归生成(Autoregressive Generation)。

Attention和KV Cache:为什么上下文不是越长越好
ransformer 架构里最核心的东西:Attention(注意力机制)。最核心的三个角色:Query、Key、Value。
Q、K、V:搜索引擎的比喻
你在 Google 搜索的时候,发生了什么?
你输入一个搜索词——"KV Cache 是什么"
Google 拿你的搜索词去匹配所有网页的标签和关键词
匹配度高的网页,把它的内容返回给你
Attention 做的事情完全一样:
Query(查询):当前正在生成的 token 会问一个问题——"我需要什么信息来决定下一个词?"
Key(索引):前面每一个 token 都有一个标签——"我包含什么类型的信息?"
Value(内容):前面每一个 token 的实际内容——"这是我的具体信息。"
生成新 token 的时候,模型拿当前 token 的 Query,去和前面所有 token 的 Key 做匹配。匹配度高的,就把对应的 Value 拿过来,加权混合,作为生成下一个 token 的依据。

举个具体的例子。假设上下文是"小明昨天去了北京,今天他去了",现在要生成下一个 token:
当前 token 的 Query 大概在问:"谁去了哪里?"
"小明"这个 token 的 Key 标签是"人名",匹配度高 → 它的 Value 被拉过来
"北京"的 Key 是"地名",匹配度也挺高 → Value 也被拉过来
"昨天"的 Key 是"时间词",跟当前问题关系不大 → Value 的权重就低
最终模型综合了高权重的 Value 信息,可能生成"上海"(另一个城市)。
这就是 Attention 的全部了。 没有什么神秘的,就是一个"查询-匹配-取值"的过程,只不过这个过程是可以被训练的——模型通过大量数据学会了怎么生成好的 Query、Key 和 Value。
KV Cache:已经算过的 Key 和 Value,别再算了
好,理解了 Q、K、V 之后,KV Cache 就很好理解了。
刚才说模型每生成一个新 token,都要拿 Query 去匹配前面所有 token 的 Key 和 Value。但你想想,如果模型已经生成了 100 个 token,现在要生成第 101 个,它需要用到前 100 个 token 的 Key 和 Value。等到要生成第 102 个的时候,前 101 个 token 的 Key 和 Value 又要用一遍——其中 100 个跟刚才是一模一样的。
重新计算一遍?太浪费了。
所以模型会把之前算过的每个 token 的 Key 和 Value 缓存起来,这个缓存就叫 KV Cache(Key-Value Cache)。现在你知道这个名字的由来了——缓存的就是 Attention 里面的 K 和 V。

打个比方:你在考试,每道题都需要翻课本找公式。KV Cache 就像你把常用公式抄在草稿纸上——后面的题直接看草稿纸就行,不用每次都翻书。
但 KV Cache 有一个关键限制:它是基于"前缀匹配"的。
举个例子。你的 Agent 第一轮对话,发给模型的内容是:
PLAINTEXT
[System Prompt] + [用户消息1] + [模型回复1]
第二轮对话,发的是:
PLAINTEXT
[System Prompt] + [用户消息1] + [模型回复1] + [用户消息2]
因为前面的部分完全一样,只是末尾加了新内容,所以前面所有 token 的 KV Cache 都能复用。模型只需要计算新增的 [用户消息2] 的部分。
但如果你做了一件事——比如在 System Prompt 的开头加了个时间戳:
PLAINTEXT
当前时间:2026-04-02 08:00:00 ← 这个每次都变!
[其余的 System Prompt]
[用户消息1]
[模型回复1]
[用户消息2]
完蛋。第一个 token 就不一样了,后面所有的 KV Cache 全部作废,需要从头重新计算。
模型“选词”:从打分到概率
模型每生成一个 token,内部到底发生了什么?简单来说,分三步:
第一步:给所有候选词打分(Logits)
模型的词汇表里有几万个 token(Claude 大概有 10 万多个)。每生成一个新 token,模型会给词汇表里的每一个 token 打一个原始分数,表示"根据前面的上下文,这个 token 接下来出现的可能性有多大"。
这些原始分数叫 logits。
比如在"今天天气真"这个上下文后面,"好"这个 token 可能得分 5.2,"不"得分 3.1,"的"得分 1.8,而"跑步"可能只有 -2.3。
第二步:把分数变成概率(Softmax)
softmax 做的事情很简单:分数越高的词,概率越大;分数越低的词,概率越小。而且这个差距会被放大——高分词的概率会比它的原始分数暗示的还要高。
经过 softmax 之后,"好"可能变成 65% 的概率,"不"15%,"的"8%,"跑步"0.01%。

第三步:根据概率采样(Sampling)
拿到概率分布后,模型从里面"抽签"选一个 token。你可能会说了:直接拿概率最高的就可以了嘛!
但事实并非如此,概率高的只是更容易被选中,但不是确定的——这就是为什么你问模型同一个问题,有时候得到不同的回答。反过来思考,如果每次都拿概率最高了,那模型的输出基本就是那些 token 序列,就跟复读机一样,没啥智能可言了。
这里提一个你肯定用过的参数:temperature。
说个冷知识,temperature 调的是 softmax 输出的概率分布的"尖锐程度":
temperature 接近 0:概率分布变得非常尖锐,高的特别高,低的也很低,最高分的那个 token 几乎 100% 被选中。模型输出变得确定、保守。
temperature = 1:正常的概率分布,有一定的随机性。
temperature > 1:概率分布变得更平坦(或者更平均),低分 token 也有机会被选中。模型输出变得更有创意,但也更容易胡说八道。
做 Agent 的时候,一般用比较低的 temperature(0 或者接近 0),因为你希望模型做可靠的决策,而不是来一段"创意写作"。
这个"打分 → 概率 → 采样"的过程,你就能理解两个在 Agent 里非常关键的技术:
Logit Mask(约束解码)
比如我只希望模型输出 "yes" 或 "no",那我就把除了 "yes" 和 "no" 之外的所有 token 的 logit 设为负无穷(经过 softmax 后概率就是 0 了)。模型只能在 "yes" 和 "no" 之间选。
这就叫 logit mask,也叫约束解码。
Manus 在多 Agent 架构里用这个技术来确保子 Agent 的输出格式严格可控——不管模型"想"说什么,logit mask 保证它只能按规定的格式输出。
Structured Output(结构化输出)
同理,当你让模型输出一个 JSON 格式的工具调用时,模型怎么保证输出的 JSON 是合法的?
一种方式就是在每一步生成的时候,用 logit mask 只允许当前上下文下语法合法的 token。比如刚输出了 {"name": 之后,下一个 token 只能是引号 " 开头的字符串。
这就是为什么现在的模型能比较可靠地输出结构化的工具调用参数——不是它"理解"了 JSON 语法,而是在解码阶段被约束了。
总结一下:
看看一次 Agent 的工具调用到底经历了什么:
Tokenize:你的 prompt(system prompt + 对话历史 + 用户指令)被拆成一串 token 数字
Attention 计算:模型回看所有 token,给每个 token 分配注意力权重,综合所有信息
KV Cache:前面轮次已经算过的 token 直接复用缓存,只算新增的部分
生成 token:模型输出第一个 token 的 logits → softmax → 采样,得到第一个 token
自回归循环:把新 token 加入上下文,回到第 2 步,重复直到输出完整的工具调用 JSON
约束解码(如果用了的话):在第 4 步的采样阶段,通过 logit mask 限制模型只能输出合法的 token


