2024 年,我做了一个面向内部同事的"AI 助手"。第一版,本质上就是给大模型套了一个聊天框:用户在输入框里问什么,我把这句话原样发给大模型,它回什么,我就在界面上显示什么——一问一答,干净利落。一开始,大家用它问概念、改文案、润色邮件,都挺满意。可没过多久,需求就开始"越界"了。有人问:"帮我查一下我们服务器现在的磁盘使用率,超过 80% 就告诉我哪个目录最占地方。"——大模型答不了,它没有任何办法去"查"一台真实的服务器。有人问:"把这周三个项目的周报汇总成一段话。"——它也答不了,那三份周报在我们的系统里,它根本读不到。我起初还在 prompt 上较劲,想把"能力"写进提示词里,结果当然是徒劳——你没法靠"嘴说",就让一个只会生成文字的模型,真的去读一个文件、调一个接口。我盯着这些它答不了的需求,慢慢想明白了一件事:我做的根本不是什么"AI 助手",我做的只是一个"AI 聊天框"。一个真正的助手,不能只会"说",它还得会"做"——会自己判断这件事该用哪个工具、会把一个大任务拆成几步、会看着上一步的结果决定下一步怎么走。这个"会做事"的东西,有个名字,叫 Agent(智能体)。这件事逼着我把 Agent 到底比 chatbot 多了什么、那个让它"边想边做"的 ReAct 范式是怎么回事、工具该怎么设计、循环该怎么控制、以及工程上的坑,彻底理清了一遍。本文是这份梳理的完整复盘。
问题背景:一个"只会聊天、不会做事"的 AI 助手
需求:做一个能帮内部同事干活的"AI 助手"
我的方案 A:给大模型套一个聊天框 —— 用户问什么,原样发给
模型,它答什么就显示什么(一问一答)
能用的场景:
- ★ 问概念、改文案、润色邮件 —— 没问题,大家挺满意
很快就答不了的需求:
- ★★ "查一下服务器磁盘使用率,超 80% 就告诉我哪个目录最占地"
—— 模型没法去"查"一台真实服务器
- ★★ "把这周三个项目的周报汇总成一段话"
—— 那三份周报在我们系统里,模型读不到
★ 我在 prompt 上较劲,想把"能力"写进提示词 —— 徒劳。
你没法靠"嘴说",让一个只会生成文字的模型,真的去
读文件、调接口。
★★ 想明白的:我做的不是"AI 助手",只是"AI 聊天框"。
真正的助手不能只会"说",还得会"做" —— 会判断该用
哪个工具、会把大任务拆成几步、会看着上一步结果决定
下一步。这个"会做事"的东西,叫 Agent(智能体)。
★ 本文要做的:把 Agent 比 chatbot 多了什么、ReAct 范式、
工具设计、循环控制、工程坑,彻底讲透。
什么是 Agent:从"一问一答"到"思考-行动"循环
# === ★ 先把 Agent 和普通 chatbot 的界线,划清楚 ===
# === ★ 普通 chatbot:一次性的"问 -> 答" ===
# ★ ★ 它的链路只有一步:接收问题 -> 模型生成回答 -> 结束。
# 它的全部能力,就锁死在"模型脑子里已有的知识"和
# "它生成文字"这一个动作里。
# ★ ★ 所以,凡是需要"接触外部世界"的事 —— 查实时数据、
# 读一个文件、调一个 API、算一道精确的数学题 —— 它
# 一概做不了。它只会"说",不会"做"。
# === ★★ Agent:一个"会循环干活"的系统 ===
# ★ ★ Agent 不是一个更聪明的模型,它是一个【系统】。这个
# 系统由三部分组成:
# - ★【大脑】:一个大模型,负责"思考、做决策";
# - ★【手脚】:一组工具(Tools)—— 查数据库、读文件、
# 调接口、执行计算……每个工具是一段【真实的代码】;
# - ★【循环】:一个把"大脑"和"手脚"串起来反复运转的
# 控制流程。
# ★ ★★ 关键差别:chatbot 是"问一次、答一次"的【直线】;
# Agent 是"思考 -> 行动 -> 观察结果 -> 再思考"的
# 【循环】,它会一圈一圈转,直到任务真正完成。
# === ★ 一个 Agent 处理任务的样子 ===
# ★ ★ 还是"查磁盘使用率"那个需求。Agent 会这样转:
# - 思考:"我需要先拿到磁盘数据" -> 行动:调"查磁盘"
# 工具 -> 观察:拿到"/data 占了 85%";
# - 再思考:"85% 超 80% 了,用户要知道哪个目录最占地"
# -> 行动:调"查目录大小"工具 -> 观察:拿到结果;
# - 再思考:"信息够了" -> 给出最终回答。
# ★ 它把一个需求,拆成了【若干步】,每步【做一个动作、
# 看一个结果】,再决定下一步 —— 这就是 Agent 的本质。
# === ★ 一个根子上的认知:模型只"出主意",不"动手" ===
# ★ ★★ 划重点:Agent 里,模型【从不亲自执行任何工具】。
# 它只是输出一句"我决定调用 X 工具、参数是 Y"。真正
# 去执行 X 的,是【你的程序】。模型负责"决策",程序
# 负责"执行" —— 这条边界,是理解整个 Agent 的地基。
# === 小结 ===
# ★ 普通 chatbot 是一次性的"问->答"直线:接收问题、模型
# 生成回答、结束,能力锁死在模型已有知识和生成文字这
# 一个动作里,凡是需要接触外部世界的事(查实时数据、
# 读文件、调 API、算精确数学题)它一概做不了,只会说
# 不会做。★★ Agent 不是更聪明的模型而是一个系统,由
# 三部分组成:大脑(一个大模型负责思考决策)、手脚
# (一组工具 Tools,每个工具是一段真实代码)、循环
# (把大脑和手脚串起来反复运转的控制流程);关键差别
# 是 chatbot 是问一次答一次的直线,Agent 是"思考->行动
# ->观察结果->再思考"的循环,一圈圈转直到任务完成。
# ★ Agent 处理任务的样子:把一个需求拆成若干步,每步做
# 一个动作、看一个结果再决定下一步。★★ 一个根子上的
# 认知:Agent 里模型从不亲自执行任何工具,它只输出
# "我决定调用 X 工具参数是 Y",真正执行的是你的程序
# —— 模型负责决策、程序负责执行,这条边界是地基。
ReAct:让模型"边想边做"的核心范式
# === ★ Agent 的循环,具体怎么转?答案是 ReAct ===
# === ★ ReAct = Reasoning(推理)+ Acting(行动) ===
# ★ ★ ReAct 是目前最主流的 Agent 工作范式。它的核心,是
# 要求模型在【每一步】,都同时产出两样东西:
# - ★ Thought(思考):用自然语言,写下"我现在的分析、
# 我下一步打算干什么、为什么"。
# - ★ Action(行动):一个明确的指令 ——"调用哪个工具、
# 传什么参数"。
# ★ ★ "先想、再做",而且把"想"也【显式地写出来】——
# 这就是 ReAct 这个名字的由来。
# === ★ 一个 ReAct 循环,完整转一圈是这样的 ===
# ★ ★ ① 模型输出 Thought + Action(它"想"完、决定"做"啥);
# ★ ★ ② 你的程序,解析出 Action,【真正执行】那个工具;
# ★ ★ ③ 程序拿到执行结果,包装成一条 Observation(观察);
# ★ ★ ④ 把这条 Observation,【追加回】给模型的上下文;
# ★ ★ ⑤ 模型读到这条新的 Observation,产出【下一轮】的
# Thought + Action…… 如此循环。
# ★ 直到某一轮,模型认为信息够了,输出的不再是 Action,
# 而是一个 Final Answer(最终答案)—— 循环结束。
# === ★★ 为什么"把思考显式写出来"这么重要 ===
# ★ ★ 你可能觉得 Thought 是多余的 —— 直接让模型输出
# Action 不就行了?★★ 不行。让模型先用文字"想一遍",
# 是在【强迫它一步步推理】,而不是凭直觉一下子跳到
# 某个动作。这能显著提升它选对工具、传对参数的概率。
# ★ ★ 而且,Thought 是【可读的】。Agent 出了错,你翻
# 它每一步的 Thought,就能清清楚楚看到它"是在哪一步、
# 因为想错了什么,而走偏的" —— 这是排查 Agent 的命脉。
# === ★ 这个范式靠什么"框住"模型 ===
# ★ ★ ReAct 不是模型天生就会的。它靠【system prompt】
# 实现:你在系统提示词里,把"可用工具有哪些""你必须
# 严格按 Thought / Action / Observation 这个格式输出"
# 一条条写清楚。模型据此,就按这个套路来。
# === 小结 ===
# ★ Agent 的循环具体怎么转,答案是 ReAct = Reasoning
# 推理 + Acting 行动,它是目前最主流的 Agent 工作范式,
# 核心是要求模型每一步同时产出两样东西:Thought 思考
# (用自然语言写下我现在的分析、下一步打算干什么、为
# 什么)和 Action 行动(一个明确指令:调哪个工具传什么
# 参数)—— 先想再做且把想也显式写出来。★ 一个 ReAct
# 循环转一圈:① 模型输出 Thought+Action;② 程序解析出
# Action 真正执行那个工具;③ 程序拿到结果包装成一条
# Observation;④ 把 Observation 追加回模型上下文;
# ⑤ 模型读到新 Observation 产出下一轮 Thought+Action,
# 如此循环,直到某轮模型输出的不是 Action 而是 Final
# Answer 最终答案,循环结束。★★ 为什么把思考显式写出来
# 重要:让模型先用文字想一遍是强迫它一步步推理而不是
# 凭直觉跳到某动作,显著提升选对工具传对参的概率;且
# Thought 可读,出错时翻每步 Thought 就能看到它在哪步
# 想错走偏,是排查命脉。★ 这个范式靠 system prompt
# 框住模型:把可用工具、必须严格按格式输出写进系统
# 提示词,模型据此按套路来。
# ★ ReAct 范式:模型每一步严格按 Thought / Action 格式输出
# 我们在 system prompt 里,要求模型这样回复:
#
# Thought: <我现在的分析,下一步该干什么>
# Action: <工具名>
# Action Input: <传给工具的参数,JSON 格式>
#
# 或者,当它认为任务已经完成:
#
# Thought: <我已经掌握了足够的信息>
# Final Answer: <给用户的最终回答>
import json
def parse_react_reply(reply: str):
# ★ 若模型给出 Final Answer,说明它认为任务结束了
if 'Final Answer:' in reply:
answer = reply.split('Final Answer:', 1)[1].strip()
return {'type': 'final', 'answer': answer}
# ★ 否则,从回复里抽出它选的工具名 和 参数
tool = reply.split('Action:', 1)[1].split('\n', 1)[0].strip()
raw_args = reply.split('Action Input:', 1)[1].strip()
# ★★ 模型给的 JSON 未必规范 —— 解析失败要兜底,回给模型让它重写
try:
args = json.loads(raw_args)
except json.JSONDecodeError:
return {'type': 'bad_format',
'hint': 'Action Input 不是合法 JSON,请重新输出'}
return {'type': 'tool', 'tool': tool, 'args': args}
工具(Tools)怎么设计:Agent 的手脚好不好用
# === ★ Agent 能力的上限,很大程度由"工具"决定 ===
# === ★ 一个工具,要定义清楚三样东西 ===
# ★ ★ 每个工具,本质是一段你写的真实代码。但要让模型
# "会用"它,你得给它配一份清楚的"说明书",含三样:
# - ★ name(名字):简短、能见名知意,如 get_disk_usage;
# - ★ description(描述):这个工具【是干什么的、什么
# 时候该用】 —— 这是写给【模型】看的;
# - ★ parameters(参数表):它需要哪些入参,每个参数
# 什么类型、什么含义。
# === ★★ description 是重中之重:它是模型的"选择依据" ===
# ★ ★ 模型【凭什么】在一堆工具里选中某一个?它【唯一】
# 的依据,就是你写的 description。它不会看你的代码,
# 它只读这段描述。
# ★ ★★ 所以:description 写得含糊,模型就会"该用时不用、
# 不该用时乱用、用了传错参数"。把它当成"写给一个新
# 同事的操作说明"来写 —— 说清楚【适用场景】,甚至给
# 一两个【例子】,模型的工具选择准确率会明显上升。
# === ★ 工具数量:不是越多越好 ===
# ★ ★ 你给模型挂 30 个工具,它每一步都要在 30 个里面挑 ——
# 选错的概率,随工具数量明显上升。★ 原则:只挂这个
# Agent【真正需要】的工具;功能相近的,考虑合并成一个。
# === ★★ 工具的"错误返回",必须让模型看得懂 ===
# ★ ★ 工具执行,一定会有失败:参数错了、网络超时、查无
# 此数据。这时【最忌讳】的,是直接抛一个异常、让整个
# Agent 崩掉。
# ★ ★★ 正解:把错误,也"翻译"成一条【模型能读懂的
# Observation】 —— 比如返回 {"error": "主机名不存在,
# 可用主机有 ..."}。模型读到这条结构化的错误,就有
# 机会【自己分析、换个参数重试】。一个会"把失败说
# 清楚"的工具,才能让 Agent 拥有"自我修正"的能力。
# === 小结 ===
# ★ Agent 能力的上限很大程度由工具决定。★ 一个工具要
# 定义清楚三样:name 名字(简短见名知意如
# get_disk_usage)、description 描述(这工具是干什么、
# 什么时候该用,写给模型看)、parameters 参数表(需要
# 哪些入参、每个什么类型什么含义)。★★ description 是
# 重中之重:模型凭什么在一堆工具里选中某一个,它唯一
# 的依据就是 description,它不看你的代码只读这段描述
# —— 写得含糊模型就会该用时不用、不该用时乱用、传错
# 参数,要当成"写给新同事的操作说明"来写,说清适用
# 场景甚至给一两个例子。★ 工具数量不是越多越好,挂
# 30 个每步都要在 30 个里挑选错概率明显上升,只挂真正
# 需要的、功能相近的考虑合并。★★ 工具的错误返回必须
# 让模型看得懂:执行一定会失败(参数错、超时、查无
# 数据),最忌讳直接抛异常让 Agent 崩掉,正解是把错误
# 也翻译成模型能读懂的结构化 Observation(如 {"error":
# "主机名不存在,可用主机有..."}),模型读到才有机会
# 自己换参数重试 —— 会把失败说清楚的工具,才能让
# Agent 拥有自我修正的能力。
# ★ 工具定义:name + description + 参数 —— description 是给模型看的"说明书"
TOOLS = {
'get_disk_usage': {
# ★★ description 写不好,模型就会该用时不用、不该用时乱用
# 要写清【适用场景】,把它当成给新同事的操作说明
'description': '查询指定服务器的磁盘使用率。当用户问到磁盘、存储'
'空间、目录占用情况时使用。返回各挂载点的使用百分比。',
'parameters': {
'host': {'type': 'string', 'description': '服务器主机名或 IP'},
},
'func': lambda host: query_disk(host),
},
'get_dir_size': {
'description': '查询某台服务器上,指定路径下各子目录的占用大小。'
'通常在磁盘使用率偏高、需要定位"谁最占地"时使用。',
'parameters': {
'host': {'type': 'string', 'description': '服务器主机名或 IP'},
'path': {'type': 'string', 'description': '要排查的目录路径'},
},
'func': lambda host, path: query_dir_size(host, path),
},
}
def execute_tool(name, args):
# ★★ 坑:模型可能"幻觉"出一个不存在的工具名 —— 必须先校验
if name not in TOOLS:
return {'error': f'工具 {name} 不存在。可用工具:{list(TOOLS)}'}
try:
result = TOOLS[name]['func'](**args)
return {'ok': True, 'data': result}
except TypeError as e:
# ★ 参数对不上 —— 把"该传什么参数"明确告诉模型
return {'ok': False,
'error': f'参数错误:{e}。该工具参数:{TOOLS[name]["parameters"]}'}
except Exception as e:
# ★★ 关键:别让异常直接抛出 —— 把错误"翻译"成模型能读懂的 Observation,
# 模型看到结构化的错误,才有机会自己换个参数重试
return {'ok': False, 'error': str(e)}
Agent 的循环控制:让它转得起来,也停得下来
# === ★ 大脑有了、手脚有了 —— 还差那个"反复运转的循环" ===
# === ★ 这个循环,本质是一个 while 循环 ===
# ★ ★ 把前面的东西串起来:一个 while,每转一圈做这几件事:
# - ① 把【当前的全部对话历史】发给模型,要它输出下一步;
# - ② 解析模型的回复:是要调工具,还是给最终答案?
# - ③ 若是调工具 -> 执行它 -> 把结果当 Observation,
# 【追加进历史】 -> 进入下一圈;
# - ④ 若是最终答案 -> 跳出循环,返回给用户。
# === ★★ 历史(history)是怎么"滚雪球"的 ===
# ★ ★ 这个循环里,最关键的一个变量,是那份【一直在增长】
# 的对话历史。它从"system prompt + 用户问题"开始,每转
# 一圈,就往里追加一轮"模型的 Thought+Action"和"程序
# 回填的 Observation"。
# ★ ★★ 为什么必须把历史全带上?因为模型【没有记忆】。它
# 要决定第 5 步干什么,唯一的依据,就是你这次发给它的、
# 那份【包含了前 4 步全过程】的历史。少带一段,它就
# "失忆"一段,决策必然出错。
# === ★★ 终止条件:循环必须有两个"出口" ===
# ★ ★ 出口一(正常):模型某一轮输出了 Final Answer ——
# 它认为信息够了,任务完成。这是我们期望的结束方式。
# ★ ★★ 出口二(兜底):达到【最大步数 max_steps】。你
# 必须给循环设一个步数上限。因为模型完全可能【陷入死
# 循环】 —— 比如反复调同一个工具、或在两个工具间来回
# 横跳,永远不输出 Final Answer。
# ★ ★ 没有 max_steps 的 Agent,是一个【危险品】:它会一
# 圈圈空转,而每一圈都是一次真金白银的大模型调用 ——
# 一个 bug,就能在你没察觉时烧掉一大笔钱。
# === ★ 达到上限后,别"裸退" ===
# ★ ★ 循环因为步数耗尽而退出时,别直接抛个错了事。更好
# 的做法:把"我没能在规定步数内完成"这件事,连同已经
# 拿到的中间信息,组织成一句【对用户有交代】的话返回。
# === 小结 ===
# ★ Agent 的循环本质是一个 while 循环,每转一圈:① 把当前
# 全部对话历史发给模型要它输出下一步;② 解析回复是调
# 工具还是给最终答案;③ 若调工具就执行、把结果当
# Observation 追加进历史、进入下一圈;④ 若是最终答案就
# 跳出循环返回。★★ 这个循环里最关键的变量是那份一直
# 增长的对话历史:它从 system prompt+用户问题开始,每圈
# 追加一轮模型的 Thought+Action 和程序回填的 Observation
# —— 必须全带上,因为模型没有记忆,它决定第 5 步干什么
# 唯一依据就是这份包含前 4 步全过程的历史,少带一段就
# 失忆一段。★★ 终止条件必须有两个出口:出口一正常 ——
# 模型某轮输出 Final Answer;出口二兜底 —— 达到最大步数
# max_steps,必须设这个上限,因为模型可能陷入死循环
# (反复调同一工具、两个工具间横跳),没有 max_steps 的
# Agent 是危险品,一圈圈空转每圈都是一次真金白银的调用,
# 一个 bug 就能烧掉一大笔钱。★ 达到上限后别裸退,把"没
# 能在规定步数内完成"连同中间信息组织成一句对用户有
# 交代的话返回。
# ★ Agent 主循环:把"大脑、手脚"串起来,并用 max_steps 兜底
SYSTEM_PROMPT = '''你是一个会使用工具的助手。每一步,严格按下面格式回复:
Thought: <你的分析,下一步该干什么>
Action: <工具名>
Action Input: <JSON 格式的参数>
当你掌握了足够信息,改为输出:
Thought: <我已经可以回答了>
Final Answer: <给用户的最终回答>
'''
def run_agent(question, max_steps=8):
# ★ history 是会"滚雪球"的对话历史 —— 模型没记忆,全靠它
history = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': question},
]
# ★★ 出口二(兜底):必须有步数上限,否则模型死循环会一直烧钱
for step in range(max_steps):
# ① 把【当前全部历史】发给模型,要它产出下一步
reply = call_llm(history)
history.append({'role': 'assistant', 'content': reply})
# ② 解析:它是要调工具,还是已经能给答案了?
parsed = parse_react_reply(reply)
# ★ 出口一(正常):模型给出 Final Answer,任务完成
if parsed['type'] == 'final':
return parsed['answer']
# ★ 模型 JSON 格式错了 —— 把错误回填,让它下一轮重写
if parsed['type'] == 'bad_format':
history.append({'role': 'user',
'content': f'Observation: {parsed["hint"]}'})
continue
# ③ 真正执行工具 —— 注意:执行的是【程序】,不是模型
result = execute_tool(parsed['tool'], parsed['args'])
# ★★ 把执行结果当 Observation,【追加回历史】,进入下一圈
obs = json.dumps(result, ensure_ascii=False)
history.append({'role': 'user', 'content': f'Observation: {obs}'})
# ★ 出口二收尾:步数耗尽也别"裸退",给用户一个交代
return '抱歉,我在规定步数内没能完成这个任务,请把问题拆得更具体些再试。'
工程坑:让 Agent 跑稳的那些细节
# === ★ Agent 能转起来不难,转得稳、转得省,才是真功夫 ===
# === ★★ 坑 1:上下文爆炸 —— 历史会把窗口撑爆 ===
# ★ ★ 上一节说历史会"滚雪球"。问题来了:大模型的上下文
# 窗口是【有限】的。一个多步任务,Observation 一轮轮堆
# 进去,历史很容易就【超出窗口】 —— 轻则报错,重则每
# 一步都在为一大坨历史付 token 费,又慢又贵。
# ★ ★★ 解法:给历史做【裁剪】。最朴素有效的策略 —— 永远
# 保住 system prompt 和【最初的用户问题】(这是任务的
# "根"),中间那些旧的 Observation 只保留最近几轮;太旧
# 的,要么丢弃,要么先让模型把它【压成一句摘要】再留下。
# === ★ 坑 2:错误级联 —— 一步错,步步错 ===
# ★ ★ Agent 是【串行】的:第 3 步的输入,是第 2 步的
# Observation。所以第 2 步的工具一旦返回了垃圾,第 3 步
# 就基于垃圾做决策,错误会【一路滚下去】。
# ★ ★ 这正是上一节强调"工具要返回结构化错误"的原因 ——
# 一个看得懂的错误 Observation,能让模型在【下一步】就
# 察觉、自我修正,把级联在源头掐断。
# === ★★ 坑 3:模型幻觉工具 —— 调用根本不存在的工具 ===
# ★ ★ 模型完全可能"一本正经"地输出一个你【从没提供过】
# 的工具名,或者给一个存在的工具传了【它臆想出来】的
# 参数。
# ★ ★★ 所以 execute_tool 里那两道校验(工具名在不在、
# 参数对不对)不是可选项,是【必需品】。校验不过,不要
# 崩 —— 把"没这个工具""参数不对"作为 Observation 回填,
# 给模型一个改正的机会。
# === ★ 坑 4:成本与延迟 —— 每一步都是一次大模型调用 ===
# ★ ★ 想清楚:Agent 转 N 步,就是【N 次】大模型调用。它
# 天然比"一问一答"慢 N 倍、贵 N 倍。
# ★ ★ 应对:① 用 max_steps 把步数死死摁住;② 给工具写
# 清楚的 description,让模型少走弯路、少转几圈;③ 如果
# 某几个工具调用之间【互不依赖】,可以考虑让它们并行,
# 而不是傻傻地一个等一个。
# === ★★ 坑 5:别什么都做成 Agent ===
# ★ ★ 这是最重要、也最容易被忽略的一条。Agent 的价值,在
# 于处理那些【步骤不固定、要随机应变】的任务。
# ★ ★★ 如果一个流程是【固定】的 —— 比如"先查 A、再查 B、
# 把两个结果拼起来" —— 那就【别用 Agent】。直接写一段
# 确定的代码去顺序调用,又快、又稳、又便宜。硬把一个
# 确定的流程,交给模型每一步去"现想",是用昂贵的不确
# 定性,去解决一个本不存在的问题。
# === 认知 ===
# ★ Agent 能转起来不难,转得稳转得省才是真功夫。★★ 坑 1
# 上下文爆炸:历史会滚雪球但大模型上下文窗口有限,多步
# 任务 Observation 一轮轮堆进去很容易超窗口 —— 轻则报错
# 重则每步为一大坨历史付 token 费又慢又贵;解法是给历史
# 裁剪,永远保住 system prompt 和最初的用户问题(任务的
# 根),中间旧 Observation 只留最近几轮、太旧的丢弃或先
# 压成一句摘要。★ 坑 2 错误级联:Agent 是串行的,第 3 步
# 输入是第 2 步的 Observation,一步返回垃圾后面就基于垃圾
# 决策错误一路滚 —— 这正是工具要返回结构化错误的原因,
# 看得懂的错误能让模型下一步就自我修正。★★ 坑 3 模型
# 幻觉工具:可能输出从没提供过的工具名或臆想的参数,所以
# execute_tool 里工具名校验和参数校验是必需品,校验不过
# 别崩、把错误作为 Observation 回填给模型改正机会。★ 坑 4
# 成本与延迟:Agent 转 N 步就是 N 次大模型调用,天然比
# 一问一答慢 N 倍贵 N 倍;应对是 max_steps 摁住步数、给
# 工具写清楚 description 让模型少转圈、互不依赖的工具调用
# 考虑并行。★★ 坑 5 别什么都做成 Agent:这是最重要也最易
# 被忽略的一条 —— Agent 的价值在于处理步骤不固定要随机
# 应变的任务,如果流程是固定的(先查 A 再查 B 拼起来)就
# 别用 Agent,直接写确定代码顺序调用又快又稳又便宜;硬把
# 确定流程交给模型每步现想,是用昂贵的不确定性去解决一个
# 本不存在的问题。
# ★ 坑1 解法:裁剪对话历史 —— 保住"任务的根",只留最近几轮
def trim_history(history, keep_recent=6):
# ★★ 第 1、2 条永远保留:system prompt + 最初的用户问题
# —— 这是整个任务的"根",丢了模型就彻底迷失方向
head = history[:2]
rest = history[2:]
# ★ 中间部分没超量 —— 原样返回,不折腾
if len(rest) <= keep_recent:
return history
# ★★ 超量了:旧的部分压成一句摘要,只留最近 keep_recent 轮
old, recent = rest[:-keep_recent], rest[-keep_recent:]
summary = summarize_with_llm(old) # ★ 让模型把旧历史浓缩成一段话
digest = {'role': 'user',
'content': f'Observation: (前面若干步的过程摘要){summary}'}
# ★ 新历史 = 根 + 一句摘要 + 最近几轮的完整记录
return head + [digest] + recent
# ★ 在主循环里:每追加一轮 Observation 之后,顺手裁一刀 ——
# history = trim_history(history)
# 这样无论任务转多少步,发给模型的历史长度始终可控
命令速查
AI Agent:从"会聊天"到"会做事"
=============================================================
Agent vs chatbot chatbot 是"问一次答一次"的直线;Agent 是
"思考-行动-观察-再思考"的循环,转到任务完成
三个部件 大脑(LLM 做决策)+ 手脚(Tools 真实代码)
+ 循环(把两者串起来反复运转的控制流程)
铁律 模型只"出主意"(决定调哪个工具),真正
"动手"执行的永远是你的程序
ReAct 范式
-------------------------------------------------------------
ReAct = Reasoning(推理)+ Acting(行动)
每一步 Thought(写下分析)+ Action(调哪个工具/传啥参)
一圈 Thought+Action -> 程序执行 -> Observation
-> 回填历史 -> 模型读到后产出下一轮
结束 模型输出 Final Answer,不再是 Action
工具(Tools)设计
-------------------------------------------------------------
三要素 name + description + parameters
description 模型选工具的唯一依据,当成"给新同事的说明书"写
数量 不是越多越好,只挂真正需要的,相近的合并
错误返回 别抛异常,翻译成模型读得懂的结构化 Observation
循环控制 + 五个坑
-------------------------------------------------------------
两个出口 Final Answer(正常)/ 达到 max_steps(兜底)
坑1 上下文爆炸 历史滚雪球会撑爆窗口 -> 裁剪 + 旧历史压摘要
坑2 错误级联 一步错步步错 -> 工具返回结构化错误,早修正
坑3 幻觉工具 校验工具名和参数,不过就回填错误别崩
坑4 成本延迟 转 N 步 = N 次调用 -> 摁住步数 + 工具描述清楚
坑5 别滥用 流程固定的任务用确定代码,别交给 Agent
口诀:模型负责"决策",程序负责"执行" —— 这条边界是地基
给循环设 max_steps,没有上限的 Agent 是危险品
步骤固定就别用 Agent,确定的事交给确定的代码
避坑清单
- 普通 chatbot 是"问一次答一次"的直线,能力锁死在模型已有知识里,凡是要接触外部世界(查实时数据、读文件、调接口)的事一概做不了
- Agent 不是更聪明的模型,而是一个系统:大脑(LLM 做决策)+ 手脚(Tools,每个是一段真实代码)+ 循环(把两者串起来反复运转)
- Agent 里模型从不亲自执行任何工具,它只输出"我决定调用 X 工具、参数是 Y",真正执行的是你的程序——模型负责决策、程序负责执行
- ReAct 要求模型每一步同时产出 Thought(显式写下分析)和 Action(调哪个工具),先想再做能显著提升它选对工具、传对参数的概率
- 工具的 description 是模型选工具的唯一依据,它不看你的代码只读这段描述,写得含糊模型就会该用时不用、不该用时乱用、传错参数
- 工具数量不是越多越好,挂 30 个工具模型每步都要在 30 个里挑、选错概率明显上升,只挂这个 Agent 真正需要的、功能相近的合并
- 工具执行失败时绝不能直接抛异常让 Agent 崩掉,要把错误翻译成模型读得懂的结构化 Observation,模型才有机会自己换参数重试
- 对话历史会"滚雪球",模型没有记忆全靠这份历史决策,但它很容易撑爆上下文窗口,必须裁剪——保住任务的根、旧历史压成摘要
- Agent 循环必须设 max_steps 上限,模型可能陷入死循环反复调同一工具,没有上限的 Agent 一个 bug 就能在你没察觉时烧掉一大笔钱
- 别什么都做成 Agent,流程固定的任务(先查 A 再查 B 拼起来)直接写确定代码又快又稳又便宜,硬交给模型每步现想是用昂贵的不确定性解决不存在的问题
总结
这一趟把 Agent 彻底理清的过程,纠正了我一个特别根本、也特别隐蔽的误解——我一直以为,把一个 AI 助手做得"更强",方向是去找一个【更聪明的模型】。我那版只会聊天的助手答不了"查磁盘""汇总周报",我下意识的反应,是觉得"模型还不够强,等它再强一点就行了"。可这趟梳理让我明白:不是模型不够强,是我【把整件事都压在了模型一个人身上】。一个只会生成文字的模型,无论多强,它也永远没法亲手去读一个文件、调一个接口——这不是"强弱"问题,是"能不能"问题。Agent 这个范式真正教给我的,根本不是"怎么让模型更强",而是【怎么不再要求模型独自扛下所有事】。它把一个任务,清清楚楚地切成了两半:一半叫"决策"——该用哪个工具、参数传什么、下一步往哪走,这个交给模型,因为这是它擅长的;另一半叫"执行"——真正去把那个工具跑起来、去碰那个真实的外部世界,这个【牢牢攥在我自己的程序手里】,因为这是模型永远做不到、也不该让它做的。模型只"出主意",程序负责"动手"——这条边界一旦划清,我那个卡了很久的难题豁然开朗:我不需要一个"无所不能的模型",我只需要一个"会做决策的模型",外加一组"我自己写的、确定可靠的工具"。我忽然意识到,这个"分工"的思路,我其实早就见过。RAG 是同一个思路:模型不擅长记住海量、实时的知识,那就别让它记,把知识放在外部的检索系统里,模型只负责"基于检索回来的内容作答"。Function Calling 也是同一个思路:模型不擅长精确计算和调用接口,那就别让它算,它只负责"决定该调什么",真正的调用交给程序。它们和 Agent,讲的是同一件事:【想清楚哪些事该交给模型,哪些事该留在模型之外】。模型有它锋利的地方——理解意图、做判断、随机应变;也有它根本性的短板——没有记忆、不能动手、结果不确定、还很贵。一个好的 AI 系统,不是把所有事都扔给模型然后祈祷它够强;而是冷静地把任务拆开,让模型只做它最擅长的那部分"思考与决策",把"记忆""执行""确定性的流程"这些它的短板,统统用模型之外的、可靠的工程手段接住。这趟梳理给我的最终启发,早已超出了 Agent 本身:面对任何一个想用 AI 解决的问题,我都会先停下来,做同一道题——这件事里,到底哪一部分是【非模型不可】的判断与创造,哪一部分是【本就该用确定的代码】去稳稳兜住的执行?把这条边界划对,剩下的,无非是把模型这个"会出主意的大脑",和我自己写的"靠得住的手脚",老老实实地接到一起。AI 工程的功夫,十之八九不在那个模型上,而在这条边界划得准不准。
—— 别看了 · 2026