副标题:从一段 150 行的 Python 代码,看清 Agent 循环的每一环
一、引子:你问了一个问题,但背后跑了三遍大模型
“查一下 KV Cache 的优化方法。”
当你在 ChatGPT 里输入这样一句话,它可能直接回复你一段文字。但当我们自己写了一个最小的 Agent 系统后,才发现背后远没有那么简单。
打开我们的 Agent 循环(agent_loop.py,约 150 行 Python),你会看到这个回答的实际过程:
Round 1: 发给大模型 → 它决定"我需要查资料" → 输出 JSON: {"tool": "search_kb", "args": {"query": "KV Cache 优化"}} → 执行 search_kb → 得到结果 Round 2: 把搜索结果追加到对话 → 再发给大模型 → 它觉得信息够了 → 输出纯文字回答 → 返回给用户一次回答,背后跑了2 轮 LLM 推理、1 次工具调用。如果任务更复杂,可能会循环 3-5 轮,每轮都消耗 token 和时间。
这篇文章就从我们的代码出发,拆开 Agent 循环的每一环——不讲框架、不堆概念,只看一个最简单、能工作的 Agent 系统是怎么搭建起来的。
二、System Prompt:Agent 的"出厂设置"
AI 模型本质上是一个"聊天机器人"——你问它答。它不知道什么"工具"、“调用”、“JSON 输出”。
让它变成 Agent 的关键,是System Prompt(系统提示)。看我们的代码:
SYSTEM_PROMPT=f"""你是一个 AI Agent,拥有通过工具获取信息并执行代码的能力。{get_tools_prompt()}# 工具列表拼在这里 ## 工作流程 1. 分析用户的任务 2. 如果需要信息或计算,调用对应工具 3. 查看工具返回结果,判断是否还需要进一步操作 4. 信息足够后,整合所有信息给出最终答案 ## 回答规则 - 用中文回答 - 需要调用工具时,输出 JSON 格式并写出完整的 JSON 对象, 示例:{{"tool": "search_kb", "args": {{"query": "关键词"}}}} - 直接回答时,使用自然语言,不要输出 JSON """然后调用 LLM 时,把 system prompt 放在对话第一句:
messages=[{"role":"system","content":SYSTEM_PROMPT},{"role":"user","content":"查一下 KV Cache 的优化方法"},]大模型收到这个消息时,它"知道"的信息是:
- 我是一个 Agent——不是单纯的聊天机器人
- 我有这些工具可以用——名字和作用
- 需要工具时我应该输出 JSON——告诉代码"调什么、传什么参数"
- 不需要工具时我应该直接说话——告诉代码"这就是最终答案"
如果没有这一层 system prompt,你把同样的用户问题发给模型,它只会当成一个普通问答来回复。System Prompt 就是把 LLM 从"聊天机器人"变成"Agent"的那一层。它不需要改模型、不需要微调——只需要告诉模型"你可以做什么、该怎么做"。
为什么不能写死 if-else?
有人可能会想:为什么不让代码直接判断"用户问的是不是 KV Cache 相关"来决定调不调工具?因为 LLM 能理解的问题范围太广了——你不可能为每一种问题类型写一个判断分支。把"判断权"交给模型本身,是 Agent 系统灵活性的来源。
三、工具描述的艺术:模型是"看"到你的工具的
有了 system prompt,模型知道了自己有工具可用。但它怎么知道每个工具是干什么的?
答案就在get_tools_prompt()这个函数里。它把工具列表渲染成模型能理解的文本:
defget_tools_prompt():lines=["## 可用工具"]fortinTOOLS:lines.append(f"-{t.name}({', '.join(t.params)}):{t.description}")return"\n".join(lines)渲染出来的效果是:
## 可用工具 - search_kb(query): 在本地技术博客知识库中搜索相关内容 - run_python(code): 执行 Python 代码并返回结果,用于计算和数据处理 - read_file(path): 读取本地文件内容模型看到的就是这段纯文本。没有 schema、没有类型定义、没有 function calling 协议——就是自然语言描述。
这意味着工具描述的质量直接决定模型能不能正确调用它。两个对比:
# 差的描述 - search_kb(q): 搜索知识库 # 好的描述 - search_kb(query): 在本地技术博客知识库中搜索与 query 相关的内容, 返回最匹配的 3 个文本片段及来源。用于需要查阅本地资料的回答。差别在哪里?
- 差的描述:模型可能不知道
q是什么(query 还是 question?),也不知道搜索的是"我自己的博客"还是"百度" - 好的描述:模型知道传什么(搜索关键词)、得到什么(3 个片段)、什么时候用(需要本地资料时)
在我们的实验中,即使是这样简单的描述也足够让模型正确调用工具了。但在更复杂的场景下,工具描述可能需要精确到参数类型、取值范围、返回值格式——本质上,你是在用自然语言给模型写 API 文档。
四、Agent 循环:模型的"思考-行动-观察"
有了 system prompt 和工具描述,Agent 循环就可以转起来了。核心代码只有十几行:
messages=[{"role":"system","content":SYSTEM_PROMPT},{"role":"user","content":task},]forround_idxinrange(max_rounds):# 1. 模型"思考"result=call_llm(messages)# 2. 看它输出的内容tool_name,tool_args=parse_tool_call(result["content"])iftool_name:# 3. 行动:执行工具tool_result=tools[tool_name](**tool_args)# 4. 观察:把结果追加回对话messages.append({"role":"assistant","content":result["content"]})messages.append({"role":"user","content":f"工具返回:{tool_result}"})# → 继续循环,模型再次"思考"else:# 没有工具调用 → 这是最终答案break这个循环对应到人类的认知过程非常直观:
| Agent 循环 | 人类对应 | 说明 |
|---|---|---|
| 模型输出 | 思考 | 分析当前信息,决定下一步 |
| 解析到 JSON → 调工具 | 行动 | 用工具获取信息或执行计算 |
| 工具结果追加到对话 | 观察 | 看行动的结果是什么 |
| 继续循环 | 再思考 | 基于新信息重新判断 |
| 输出自然语言 → 结束 | 回答 | 信息够了,给出结论 |
消息是怎么"长大"的?
一个 2 轮 Agent 任务的 messages 结构会让你直观地看到"长大"的过程:
初始: system: [工具列表 + 规则] user: "查一下 KV Cache 优化方法" 第 1 轮 LLM 调用后: system: [工具列表 + 规则] user: "查一下 KV Cache 优化方法" assistant: {"tool": "search_kb", "args": {"query": "KV Cache 优化"}} ← 新增 user: 工具返回:KV Cache 的优化方法包括量化、动态上下文... ← 新增 第 2 轮 LLM 调用后: system: [工具列表 + 规则] user: "查一下 KV Cache 优化方法" assistant: {"tool": "search_kb", "args": {"query": "KV Cache 优化"}} user: 工具返回:KV Cache 的优化方法包括量化、动态上下文... assistant: KV Cache 的优化方法主要包括以下三种... ← 最终回答每次工具调用的结果都追加到对话里,后续的推理轮次可以"看到"之前做了什么、搜到了什么。这就是为什么 Agent 循环的 prompt token 会不断增长——历史在累积。
五、模型怎么决定"要不要调工具"?
这是最让人好奇的问题。我们的实验中有三组任务,正好展示了模型在这个问题上的三种判断:
任务①:知识够用 → 不调
用户问:“什么是 KV Cache?用一句话回答。”
模型的心理活动(推断):
这个我知道,KV Cache 是 Transformer 推理时的优化技术…
不需要查资料,直接回答即可。
结果:输出纯文字 → 结束。零工具调用。
任务②:需要外部信息 → 调
用户问:“搜一下 KV Cache 的优化方法。”
模型的心理活动:
我知道 KV Cache 是什么,但具体的优化方法?我的训练数据里可能不全…
有 search_kb 工具,让我查查。
结果:输出{"tool": "search_kb", "args": {"query": "KV Cache 优化"}}→ 调工具。
任务③:需要精确计算 → 组合
用户问:“查 Qwen3-8B 的 KV Cache 公式,算一下 ctx=16384 时的大小。”
模型的心理活动:
先搜公式 → 搜到了 → 要用 run_python 计算 → 用搜到的参数代入 → 输出结果
结果:两次工具调用(search_kb → run_python),信息组合后输出。
判断依据是什么?
模型做出这个判断,基于它在训练中获得的对自身知识的元认知:
| 模型觉得 | 行为 | 结果 |
|---|---|---|
| 我知道,而且确信 | 直接回答 | ① 简单问答 |
| 我知道一点,但不确定 | 调用 search_kb 验证 | ② 搜索+总结 |
| 我不知道,但可以算 | 调用 run_python | ③ 搜索+计算 |
| 我不知道,也不知道怎么用工具 | 编造答案(幻觉) | ❌ 失败案例 |
这个判断不是硬编码的,完全由模型自己决定。你给的 system prompt 只是告诉模型"你可以选择调工具",但最终"要不要调"是模型基于当前问题 + 内部知识 + 工具描述综合判断的结果。
这也是为什么我们之前实验中发现了一个关键现象:
推理模型(DeepSeek-R1-7B)在任务③中失败了——它搜到了正确的公式参数,但推理过程中觉得"我记忆中的参数才是对的",用自己的知识覆盖了工具结果。
它"知道"的参数是错的,但它"不知道自己不知道"。这就是 Agent 系统中最难解决的问题:模型对自己的知识边界判断不准。
六、工具调用失败了会怎样?
既然"判断是否调工具"交给模型,那它当然可能判断错。我们在实验中遇到了几种典型的失败模式:
模式一:搜到了,但覆盖了
Round 1: search_kb → 正确返回 "36 layers, 8 kv_heads" Round 2: 但模型内心:"我记得 Qwen3-8B 的 d_model=512" → 用自己的数据算 ❌原因:推理模型被训练成"想清楚再回答"。它对自己产出的推理链有过度自信,当工具结果和"我觉得"不一致时,倾向于相信自己的判断。
模式二:重复搜索相同内容
Round 1: search_kb("KV Cache Qwen3-8B") Round 2: search_kb("Qwen3-8B KV Cache 公式") ← 几乎一样原因:小模型的上下文注意力在增长后开始衰减——它"忘了"自己第一轮搜了什么。这不是恶意,是模型能力的客观限制。
模式三:选错工具
这种模式我们没有在实验中遇到,但在实际使用中很常见——比如应该调run_python计算时,却调了search_kb("如何计算")。
原因:工具描述不够精确。如果search_kb的描述是"搜索资料,也可以用来查计算公式",模型就会混淆。
三种模式的应对
| 失败模式 | 原因 | 改进方向 |
|---|---|---|
| 覆盖工具结果 | 推理模型的"自我推理"倾向 | 换指令模型,或在 system prompt 强调"完全相信工具结果" |
| 重复搜索 | 小模型注意力衰减 | 限制历史轮次,或在工具描述中要求一次性搜全 |
| 选错工具 | 工具描述有歧义 | 优化工具名称和描述,让用途一目了然 |
| 死循环 | 模型无法判断"信息够了" | 设置 max_rounds 硬限制,或在 system prompt 要求"必须给出最终答案或明确指出信息缺失" |
模式四:死循环——模型停不下来
这是用户最常遇到也最头疼的问题:Agent 一直在调工具,永远不输出最终答案。
Round 1: 调 search_kb → 返回结果 Round 2: "让我再搜一下" → 又调 search_kb → 返回结果 Round 3: "再确认一下" → 又调 search_kb → ... Round 4: ... Round N: 达到 max_rounds 被强制终止为什么会出现死循环?
根本原因是模型无法做出"信息够了"的判断:
- 工具返回的信息模棱两可——搜到了相关内容但和问题不是完全匹配,模型觉得"还不够,再搜一次"
- 模型本身的不自信——小模型对自己"有没有足够信息"的元认知更弱,倾向于重复搜索来"确认"
- system prompt 被误解——如果提示词强调"要充分利用工具",模型可能理解为"要多调工具几次"
- 工具链太长——复杂任务需要调 3-5 个不同工具,中间任何一环的信息不完整都会触发更多调用
怎么治?
我们代码里的max_rounds=10就是最简单粗暴的防线——无论模型怎么跑,10 轮之后强制结束。
更好的做法是在 system prompt 加一条规则:
“如果你发现连续两次调用同一工具得到了相似的结果,请直接基于已有信息给出答案,不要再继续搜索。”
但最终的治本之道还是模型本身的能力——强模型(V4、GPT-4)很少出现死循环,因为它们能更准确地判断"这些信息够了"。
幻觉 vs 死循环:一张表看懂
幻觉和死循环是最常被混为一谈的两个 Agent 失败模式,但本质完全不同:
| 对比维度 | 幻觉 | 死循环 |
|---|---|---|
| 表现 | 输出错误的回答 | 不输出回答,一直调工具 |
| 本质 | “我知道”(其实不知道) | “我不够确定”(其实可能已经够了) |
| 元认知状态 | 不知道自己不知道 | 不能确信自己知道 |
| 对用户的影响 | 收到一个错的答案 | 收不到任何答案 |
| 治标方案 | 加 RAG / 工具验证 | 设 max_rounds 硬限制 |
| 治本方案 | 换更强的模型 | 换更强的模型 |
共同根源:模型的元认知能力不足。强模型既能减少幻觉(知道什么不该说),也能减少死循环(知道什么时候该停)。弱模型两个问题都容易犯——只是在不同场景下表现出不同的"症状"。
七、AI 编程智能体——为什么有的好用,有的不好用?
读到这里,你已经理解了 Agent 循环的每一环。现在我们把视野聚焦到编程 Agent——OpenCode、Cursor、Claude Code、文心快码……为什么体验差距这么大?
主流 AI 编程 Agent 一览
| 产品 | 形态 | 底层模型 | 是否开源 | 适合场景 |
|---|---|---|---|---|
| OpenCode | 终端 Agent | 中立(支持 75+ 模型) | ✅ MIT | 通用开发,离线/涉密环境 |
| Codex CLI(OpenAI) | 终端 Agent | GPT-5.5(默认) | ✅ Apache 2.0 | 通用开发,Terminal-Bench 最高分 |
| Claude Code | 终端 Agent | Claude 专属 | ❌ | 复杂任务,全栈开发 |
| Cursor | AI IDE | 可切换(GPT/Claude) | ❌ | 日常编码,多文件编辑 |
| GitHub Copilot | IDE 插件 | OpenAI 专属 | ❌ | 代码补全,VS Code 生态 |
| Aider | 终端 Agent | 中立 | ✅ | Git 工作流,脚本开发 |
| 文心快码 Comate | IDE+Agent | 多模型接入 | ❌ | 企业级开发,全流程管控 |
| TRAE(字节) | AI IDE | 可切换 | ❌ | 个人效率,多端协同 |
| Qoder(阿里) | 桌面 Agent | 通义+第三方 | ❌ | 阿里云生态,Java 开发 |
| CodeBuddy(腾讯) | IDE 插件 | 混元+第三方 | ❌ | 腾讯生态团队 |
| ZCode 3.0(智谱) | IDE 插件 | GLM-5.2 | ❌ | 专业开发,工程稳定性 |
| MiMo Code(小米) | 终端 Agent | 中立(基于 OpenCode) | ✅ | 小米生态,语音编码 |
编程 Agent 的两种形态
市面上编程 Agent 虽多,但架构上只有两种:
① 终端 Agent(Terminal-first)
代表:OpenCode、Claude Code、Aider、MiMo Code
- 跑在终端里,不依赖 IDE,启动快资源低
- 直接操作文件系统,可以执行编译、测试等命令
- 适合远程开发、服务器开发、自动化流水线
- 对 C++/系统级开发更友好——不依赖 IDE 插件生态,直接操作文件即可
② IDE 内嵌 Agent(IDE-first)
代表:Cursor、文心快码、GitHub Copilot、TRAE
- 嵌入 VS Code / JetBrains 等 IDE
- 上下文来自你当前打开的文件和项目结构
- 补全体验流畅,适合日常编码
- 对前端/全栈开发更友好(实时预览、调试集成)
两者不是互斥的——很多开发者组合使用:终端 Agent 做大重构,IDE Agent 做日常补全。
编程 Agent 的独特之处:工具链集成决定天花板
通用 Agent 的工具是"搜索"、“计算”、“读文件”。编程 Agent 需要与开发工具链深度集成:
| 工具 | 说明 |
|---|---|
| 文件读写搜索 | 理解项目结构,跨文件读取和修改 |
| Git 操作 | 自动 commit、创建 PR、解决冲突 |
| 终端命令 | 编译、运行、测试、lint |
| LSP 查询 | 查找引用、跳转定义、获取类型信息 |
一个编程 Agent 集成的工具链越深,自主性就越强——不需要你手动"拉代码、跑测试、看报错"。这也是 OpenCode 和 Claude Code 这类终端 Agent 在复杂编码任务上表现更好的原因:它们天然可以操作终端、解析编译器输出、跨文件搜索,不需要通过 IDE 插件做中转。
限制编程 Agent 的核心因素
四个因子和通用 Agent 类似,但具体表现不同:
① 底层模型对代码的理解能力(权重 50%)
这是最大的瓶颈。编程 Agent 需要的不是"能聊天",而是真正理解代码:
| 能力维度 | 强模型(GPT-4o / Claude 4+) | 弱模型(7B 级别) |
|---|---|---|
| 语法理解 | 准确理解语言特性,生成符合规范的代码 | 经常生成语法错误或不存在的 API |
| 项目上下文 | 能跨文件理解结构和依赖关系 | 只关注当前文件,改 A 坏 B |
| 工具链操作 | git、编译、测试一条龙 | 命令格式出错,或执行危险操作 |
| Debug 推理 | 能跟踪调用栈,定位根本原因 | 只能改表面症状 |
| 代码审查 | 发现逻辑漏洞、性能问题、安全风险 | 只能检查格式和命名 |
② 工具链集成的深度(权重 25%)
模型再强,Agent 的工具链不够深,能力也发挥不出来:
- OpenCode / Claude Code能做全项目搜索替换——它们有完整的文件索引
- Aider的 Git 集成最好——每次修改前自动 diff,可以逐行确认
- Cursor的 Composer 能同时改多个文件——它在 IDE 层面拿到了整个项目的 AST 信息
③ System Prompt 中代码规范的注入(权重 15%)
编程 Agent 的 prompt 比通用 Agent 更讲究:
差的做法: "你是一个编程助手,帮助用户写代码。" 好的做法(针对 C++ 开发): "你是一个 C++ 编程助手。 - 遵循 Google C++ Style Guide - 使用智能指针代替裸指针 - 优先考虑异常安全 - 所有公有方法添加 Doxygen 注释 - 修改前先 git diff 查看当前变更"代码规范写进 system prompt,生成质量会显著提升——"企业级编程 Agent"和"个人玩具"的差距往往在 prompt 中注入的工程经验,而不是模型本身。
④ 安全与交互设计(权重 10%)
编程 Agent 比其他 Agent 有更大的破坏潜力——调错了不是返回一个结果,而是改你的源码、删你的文件、执行危险命令。好的 Agent 会在每次写入前 diff 预览、默认只读需确认才写、阻止rm -rf /等危险操作。
一个公式
编程 Agent 可用性 = 代码理解能力 × 工具链深度 × Prompt 质量 × 安全设计仍然是乘法——任何一个短板都会拖垮整体体验。
选型建议
不同开发场景适合不同的编程 Agent:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 日常编码和项目理解 | Cursor | AI IDE 上下文感知好,开箱即用,适合快速上手 |
| 复杂跨文件重构 | OpenCode / Claude Code | 终端 Agent 的全项目索引更擅长批量修改 |
| 服务器/远程开发 | OpenCode / Aider | 终端 Agent 不依赖 IDE,SSH 环境下也能用 |
| 涉密/离线环境 | OpenCode | 完全本地化,可接 DeepSeek/Ollama 等本地模型 |
| 企业级合规需求 | 文心快码 Comate | 私有化部署、数据不出 VPC,已过万家企业验证 |
| 阿里云生态开发 | Qoder | 深度绑定阿里云产品线,Java 生态优化最好 |
| 腾讯生态团队 | CodeBuddy | 微信、企微、腾讯文档无缝衔接 |
| 快速脚本和原型 | Aider / OpenCode | Git 原生工作流,启动快,适合快速迭代 |
| 多端协同开发 | TRAE(字节) | Web/Desktop/Mobile 三端覆盖,任务可云端运行 |
| 系统级/C++ 开发 | OpenCode / Claude Code | 终端 Agent 对文件操作、编译命令、构建工具更自然 |
别忘了:工具只是放大器
编程 Agent 能大幅提升效率,但它不会替你理解业务和算法。好的 Agent 用户和差的用户之间,差距往往不在工具本身:
- 知道怎么分解任务、明确边界比用哪个 Agent 更重要
- 知道怎么审查生成的代码比让 Agent 写多少更重要
- 知道什么该自己写比盲目信任更重要
这和我们前面讲的 Agent 局限性一脉相承——模型不知道自己在做什么,它只知道"下一个 token 最可能是什么"。
八、总结
拆完 Agent 循环的每一环,核心结论其实很简单:
System Prompt 是 Agent 的起点。没有它,模型只是个聊天机器人。
工具描述是模型理解工具的窗口。模型看到的不是结构化的 API 定义,而是一段自然语言描述。描述质量直接决定调用成功率。
Agent 循环就是"思考-行动-观察"的重复。模型输出 → 解析 → 调用工具 → 结果追加 → 再推理,直到模型认为信息足够为止。
模型自己决定"要不要调工具"。这不是 if-else 硬编码的,而是模型基于自身知识 + 工具描述综合判断的。
失败通常不是因为"没工具",而是模型"不知道自己不知道"。死循环的根源也是如此——模型无法判断"信息够了"。
市面上 Agent 产品的差距不在"有没有 Agent",而在模型能力和工具设计的成熟度。Agent 可用性 = 模型智能 × 工具质量 × Prompt 质量 × 安全设计,任何一个短板都会拖垮整体。
最后,用一个表总结 Agent 系统四个要素以及它们各自的重要性:
| 要素 | 权重 | 出问题了会怎样 |
|---|---|---|
| 底层模型智能 | 50% | 工具选不对、参数传错、信息判断不准 |
| 工具体系设计 | 25% | 模型面对太多工具选择困难,或描述模糊导致误用 |
| System Prompt | 15% | 行为边界模糊,输出格式不规范 |
| 安全与交互设计 | 10% | 暴露调用细节给用户,缺少循环兜底机制 |
Agent 可用性 = 模型智能 × 工具质量 × Prompt 质量 × 安全设计。这是乘法——任何一个为零,整体为零。
系列全篇(CSDN):
- 从零到一:用 AI Agent 辅助在 6GB 显卡上本地部署大模型实战
- 只有 B 级能力的大模型,怎么干出 A 级的活?
- Agent 不是更聪明的模型,而是长了手脚的模型
- 从 Ollama 到 llama.cpp:一次"降一层"的本地推理探索
- KV Cache 优化实战:6GB 显存上的每一 MB 都算数
- 从零搭建本地 RAG 系统:200 行 Python 让你的模型"带着资料回答问题"
- RAG 配置怎么调最好?6GB 显存上的 4 组对比实验 — RAG 实验
- Agent 不是工具调用器——理解 Agent 的工作机制 — 本文
附录 A:常见的 Agent 状态词
用 Agent 时,你经常会看到各种状态提示一闪而过——“Thinking…”、“Transmuting…”、“Thought”。这些提示到底在说什么?
来源一:ReAct 模式(Thought → Action → Observation)
ReAct(Reasoning + Acting)是 Agent 系统最经典的论文框架,它的三个核心状态词是:
| 状态 | 含义 | 对应我们代码中的 |
|---|---|---|
| Thought | 模型在"思考"——分析当前状态,决定下一步做什么 | 每一轮 LLM 调用的输出 |
| Action | 模型决定"行动"——调用某个工具并传参 | {"tool": "search_kb", "args": {"query": "..."}} |
| Observation | 模型"观察"——看到工具返回的结果 | 工具 search_kb 返回: ...追加到对话 |
你看到的 Agent 推理链路,本质上就是这三个词的循环:
Thought: 用户想知道 KV Cache 优化方法,我需要查资料 Action: search_kb(query="KV Cache 优化") Observation: 返回了量化和动态上下文等方法 Thought: 信息够了,我直接总结 → 最终回答来源二:推理模型的状态
推理模型(DeepSeek-R1、o1、o3 等)在输出最终答案前,会先展示一段内部推理过程:
| 状态 | 来源 | 含义 |
|---|---|---|
| Thinking… | DeepSeek-R1 / o1 | 模型正在进行内部思维链推理 |
| Reasoning… | 部分推理模型界面 | 同上,不同 UI 的叫法不同 |
对应我们代码中的reasoning_content字段。在实验数据中,R1-7B 的任务③就有大量推理内容——它在"想"怎么计算 KV Cache,但反而把它想错了。
来源三:工具执行状态
Agent 执行工具时,界面会展示当前在做什么:
| 状态 | 典型场景 |
|---|---|
| Searching… | 搜索知识库或网络(Perplexity 等) |
| Computing… | 执行代码并等待结果 |
| Executing… | 正在执行工具调用 |
| Reading… | 读取文件内容 |
| Planning… | 分解复杂任务,制定执行计划 |
| Processing… | 通用状态——信息处理中 |
| Transmuting… | 花哨的叫法,本质和 Processing 一样——模型正在处理或转换信息 |
| Verifying… | 验证结果是否正确 |
| Reflecting… | 反思之前的推理是否合理 |
和我们实验的关系
你在我们实验日志中看到的其实是"没有 UI 包装"的 ReAct 状态:
思考中... → 调用了 search_kb → 思考中... → 调用了 run_python → 思考中... → 输出最终答案每个"思考中…"背后就是一个Thought,每次工具调用就是Action,工具返回就是Observation。我们的 Agent 代码实现的就是最经典的 ReAct 模式——只是界面没有花哨的动画,只有文字日志。
附录 B:上下文窗口用完了会怎样
Agent 循环有一个隐蔽问题:对话在不断增长。每一轮 LLM 调用都会追加新的内容,几轮下来可能从几百 token 涨到几千甚至上万。
当上下文窗口接近极限时
| 表现 | 原因 | 我们的实验印证 |
|---|---|---|
| 模型开始"忘记"早期内容 | Attention 在长上下文中衰减,越靠前的内容影响越小 | R1-7B 在 Round 2 又搜了几乎一样的关键词——它"忘了"第一轮已经搜过 |
| 工具结果被忽略 | 工具返回在对话中位置靠前,模型更关注最近的对话 | R1-7B 搜到了正确参数,但推理时用了自己"记得"的错误参数 |
| System Prompt 指令被稀释 | System Prompt 在对话最开头,随对话增长被"挤"出有效注意力范围 | 输出格式开始混乱,不再遵循 JSON 规范 |
| 推理质量整体下降 | 可用上下文变少,模型只能看到局部信息 | 回答变短、更笼统、信息量下降 |
当上下文窗口被超过时
| 场景 | 发生什么 | 后果 |
|---|---|---|
| 本地部署(llama.cpp) | 物理内存不够会 OOM;--ctx-size限制超出后被截断 | 进程退出 / 回答残缺 |
| API 调用(V4 / GPT) | 返回 400 错误:“Context length exceeded” | Agent 循环中断,需手动处理 |
| 部分 API 自动截断 | 自动丢弃对话最前面的内容来腾空间 | 模型丢失 system prompt 和早期工具结果,后续推理不可靠 |
如何避免?
- 限制最大轮次:
max_rounds=10是最简单的防线——避免无限累积 - 摘要中间步骤:不把完整工具结果追加到对话,而是用一个摘要:“search_kb 返回了 3 个方法”
- 滑动窗口:只保留最近 N 轮的工具结果和推理,抛弃最早的内容
- 选择更大的上下文窗口:云端模型(如 DeepSeek V4)支持 1M 上下文,而本地 7B 模型通常只有 8K-32K——这也是强模型做 Agent 的另一个优势。
在实际使用中,一个 2-3 轮的 Agent 任务通常消耗几千 token,但随着轮次增加,上下文压力会显著放大。如果 Agent 出错需要重试,消耗还会翻倍。关于具体数字,我们会在下一篇《Agent 推理的"显微镜"》中用实测数据详细分析。