从零构建一个能思考、会搜索的 LLM Agent
当大语言模型不再是“只会接龙的鹦鹉”,而是拥有工具和推理能力的智能体。
一、为什么“让模型一步步思考”能大幅提升推理能力?
一个让所有开发者惊讶的训练发现
在没有任何微调的情况下,仅仅在提示词末尾加上**“Let’s think step by step”**,GPT-3 在数学推理任务上的准确率就能从 17.7% 飙升到 78.7%。摘自 Google Brain 团队 2022 年论文中展示的真实数据。
初看时我难以置信——这不就是让人多写几个字吗?但亲手实验后,我彻底服了。
从“黑盒一跳”到“白盒推演”
没有 CoT 时,模型的行为像这样:
问:小明有5个苹果,给小红2个,又买了3个,现在有几个?
模型内心:5 → (某种神秘计算) → 6
输出:6
你永远不知道它是怎么得出 6 的,错了也无从修正。
有了 CoT,模型变成:
问:小明有5个苹果,给小红2个,又买了3个,现在有几个?
模型输出:一开始有5个。给小红2个,剩下3个。又买了3个,3+3=6。所以答案是6。
这不仅仅是多写几个字,而是彻底改变了模型的计算路径。
深度拆解:CoT 为何有效?
我根据自己的实验和论文理解,总结了三个核心原因:
-
将复杂问题分解为原子步骤
模型本质上是一个词级别的概率模型。直接输出最终答案相当于要求它“一步跨越鸿沟”。CoT 把大跳跃变成小碎步——每一步的推理压力小得多,正确率自然更高。 -
激活了模型的“工作记忆”
标准输出时,模型只能依赖内部隐状态。而 CoT 要求它把中间结果写出来,这些文字就成了外部扩展记忆。当你写“剩下 3 个”时,后面的计算可以直接引用这个数字,不会遗忘。 -
错误可定位、可修复
有一次模型算错了,输出:“5-2=2,然后 2+3=5”。我一眼看出 5-2=2 错了。如果没有 CoT,我连哪里错都不知道。这种可解释性对调试 Agent 至关重要。
实验数据(我自己跑的)
用 10 道小学数学题测试 GPT-3.5:
| 模式 | 准确率 |
|---|---|
| 直接输出答案 | 40% |
| 加一句“Let's think step by step” | 80% |
| 加 CoT + 3 次采样投票 | 90% |
CoT 带来的提升是翻倍级的,零成本,高回报。
二、ReAct 模式:让 Agent 真正“动起来”
如果说 CoT 让模型会“思考”,那 ReAct 就是让模型 “思考 + 行动”。论文《ReAct: Synergizing Reasoning and Acting in Language Models》提出了一个简单却强大的循环:
Thought → Action → Observation → (重复) → Final Answer
我实现的一个真实案例
用户问:“《星际穿越》的导演哪一年出生的?到 2026 年他多少岁?”
没有工具的模型会瞎编:“导演是诺兰,1970 年出生,2026 年 56 岁。”——蒙对了还好,蒙错就完蛋。
用 ReAct 模式,模型的行为变成了:
Thought: 我需要先找到《星际穿越》的导演,然后查他的出生年份,最后计算年龄。
Action: search_wikipedia
Action Input: 星际穿越 导演
Observation: 搜索结果:克里斯托弗·诺兰(Christopher Nolan),1970年7月30日出生...
Thought: 导演是克里斯托弗·诺兰,出生于1970年。现在计算到2026年的年龄。
Action: calculate
Action Input: 2026 - 1970
Observation: 计算结果: 56
Thought: 我有足够信息了。
Final Answer: 导演克里斯托弗·诺兰出生于1970年,到2026年他将满56岁。
注意每次 Action 实际上是我在代码里拦截并真正调用了工具(Wikipedia API 或计算器),然后把结果以 Observation 的形式塞回给模型。模型再根据观察决定下一步。
这就是最朴素的 Agent 形态:模型负责规划,代码负责执行。
踩坑记录:格式化是魔鬼
ReAct 对格式的要求极其严格。模型必须输出:
Action: search_wikipedia
Action Input: 星际穿越 导演
稍有偏差(比如 Action Input: 写成 Action input:,或者多了个冒号,或者把 Input 写到了下一行),解析就会失败。
我学到的一课:永远在 System Prompt 里给出完美的示例,并且实现解析失败重试机制。当正则匹配不到 Action 时,返回一条 Observation:“格式错误,请严格按照 Thought/Action/Action Input 格式输出”,模型通常会自动修正。
工具的真实实现(关键代码片段)
def search_wikipedia(query):
url = "https://en.wikipedia.org/w/api.php"
params = {
"action": "query",
"list": "search",
"srsearch": query,
"format": "json",
"srlimit": 3,
}
resp = requests.get(url, params=params)
data = resp.json()
return format_search_results(data)
完全不用 LangChain,只用 requests 加 Wikipedia 官方 API,就能获得真实的搜索能力。这让我意识到:Agent 框架降低的是门槛,而不是必要性。手动实现能让人更深刻地理解每一步。
三、temperature:一个参数如何决定模型的“性格”
在实验中,我发现 temperature 对推理任务的影响常常被低估。
直观理解:从“老实人”到“疯癫诗人”
| temperature | 行为特征 |
|---|---|
| 0 | 每次都选概率最高的词,输出完全确定,像背诵标准答案 |
| 0.3~0.5 | 有一定随机性但保持逻辑,像有经验的专家 |
| 0.7~1.0 | 创意丰富但偶尔跑偏,像头脑风暴中的成员 |
| >1.2 | 输出可能胡言乱语,像醉酒后的胡话 |
对推理任务的特殊影响
- 数学题:
temperature=0最佳。稍有随机性就可能从“5-2=3”跳成“5-2=2”,一步错步步错。 - 需要多样性的任务:例如代码生成、头脑风暴,
0.7左右能产生惊喜。 - 自洽性(Self-Consistency):这正是利用高 temperature + 多次采样来获取多条推理路径,然后用投票选出最常出现的答案。原理是:不同的随机路径可能通往同一个正确答案,而错误答案分散。
我的调参经验表
| 任务类型 | 推荐 temperature | 原因 |
|---|---|---|
| 事实问答 | 0 | 确定性越高越好 |
| 数学推理 | 0 ~ 0.3 | 需要精确,但略高可为自洽性服务 |
| 创意写作 | 0.7 ~ 1.0 | 需要发散 |
| 代码生成 | 0.2 ~ 0.5 | 既要有规范,又要避免死板 |
| Agent 规划 | 0.3 | 需要稳定格式,又要有一定灵活性 |
四、亲手搭建一个 Agent 的真实感悟
我犯过的错误
- 过度依赖框架:一开始想用 LangChain,但配置复杂、抽象多,反而掩盖了基本原理。最后决定自己写解析器 + 工具调用,200 行代码搞定。
- 忘记处理工具返回的异常:Wikipedia 可能返回空结果,calculate 可能除零。如果不处理,模型会收到奇怪的 Observation,然后彻底懵掉。
- 设置过高的温度:在 Agent 的规划阶段用高 temperature,模型开始随意编造工具名,乱输出格式,完全失控。
- 上下文爆炸:每次循环都把完整的 Thought/Action/Observation 历史塞回去,很快超过 token 限制。解决方案是只保留最近的几轮,或使用更长的上下文模型。
成功的关键
- 严格的 System Prompt:用 3-5 个示例教会模型格式。
- 健壮的解析器:支持多种变体(带引号/不带,多行输入等)。
- 最大步数限制:防止 Agent 陷入无限循环。
- 清晰的彩色输出:调试时能一眼看到模型在每个步骤的思考过程。
推荐的学习路径
- 先用 API 跑通单次 CoT 调用,体会“思考”的价值。
- 手动实现两个工具函数(搜索,计算)。
- 写一个死循环:调用模型 → 解析 Action → 执行 → 拼接 Observation → 继续。
- 加入解析失败重试、最大步数、详细日志。
- 测试一个需要多步的任务(比如本文的“星际穿越导演年龄”)。
踩完这些坑,你会比任何框架文档都更理解 Agent。
写在最后
从“只会接龙”到“能搜索、能计算、能规划”,大语言模型的能力边界在被新的提示范式不断拓宽。最让我兴奋的是,这些进步不需要改变模型参数,只需要改变我们与模型对话的方式。
CoT 教会模型“慢思考”,ReAct 赋予模型“手脚”,temperature 调节模型的“性格”。当我把这三者组合起来,一个能够自主完成复杂任务的 Agent 就在几百行代码内诞生了。
附:本文所有实验代码已整理到 GitHub(可私信获取),包含:
- 带彩色输出的 ReAct Agent 完整实现
- CoT 与自洽性对比测试脚本
- Temperature 影响的可视化实验