AI Agent 完全指南:从一问一答到会规划、会用工具的智能体

2024 年我做了一个面向内部同事的"AI 助手",第一版本质就是给大模型套个聊天框:用户问什么原样发给模型、它答什么就显示什么,一问一答。问概念改文案润色邮件大家挺满意,可需求很快越界:有人要查服务器磁盘使用率、有人要汇总三个项目的周报——模型一概答不了,它没法去"查"一台真实服务器、也读不到我们系统里的文件。我起初在 prompt 上较劲想把"能力"写进提示词,徒劳——你没法靠"嘴说"就让一个只会生成文字的模型真的去读文件、调接口。慢慢想明白:我做的不是"AI 助手"只是"AI 聊天框",真正的助手不能只会"说"还得会"做"——会判断该用哪个工具、把大任务拆成几步、看着上一步结果决定下一步,这个会做事的东西叫 Agent。梳理:Agent 不是更聪明的模型而是一个系统,由大脑(LLM 做决策)、手脚(一组 Tools 每个是真实代码)、循环(把两者串起来反复运转)三部分组成;chatbot 是问一次答一次的直线,Agent 是"思考-行动-观察-再思考"的循环。核心范式 ReAct=Reasoning+Acting,要求模型每步同时产出 Thought(写下分析)和 Action(调哪个工具传啥参),程序执行后把结果包成 Observation 回填,如此循环直到模型输出 Final Answer。工具设计:name+description+parameters 三要素,description 是模型选工具的唯一依据要当成给新同事的说明书写;工具数量不是越多越好;错误返回别抛异常要翻译成模型读得懂的结构化 Observation。循环控制必须设 max_steps 上限,没有上限的 Agent 是危险品一个 bug 就能烧掉一大笔钱。五个工程坑:上下文爆炸要裁剪历史、错误级联要结构化错误、模型幻觉工具要严格校验、成本延迟转 N 步就是 N 次调用、别什么都做成 Agent 流程固定的用确定代码。一个根子上的认知:Agent 里模型从不亲自执行任何工具,它只"出主意"决定调什么,真正"动手"执行的永远是你的程序——模型负责决策、程序负责执行,这条边界是地基。它和 RAG、Function Calling 讲的是同一件事:想清楚哪些事交给模型、哪些留在模型之外。

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,确定的事交给确定的代码

避坑清单

  1. 普通 chatbot 是"问一次答一次"的直线,能力锁死在模型已有知识里,凡是要接触外部世界(查实时数据、读文件、调接口)的事一概做不了
  2. Agent 不是更聪明的模型,而是一个系统:大脑(LLM 做决策)+ 手脚(Tools,每个是一段真实代码)+ 循环(把两者串起来反复运转)
  3. Agent 里模型从不亲自执行任何工具,它只输出"我决定调用 X 工具、参数是 Y",真正执行的是你的程序——模型负责决策、程序负责执行
  4. ReAct 要求模型每一步同时产出 Thought(显式写下分析)和 Action(调哪个工具),先想再做能显著提升它选对工具、传对参数的概率
  5. 工具的 description 是模型选工具的唯一依据,它不看你的代码只读这段描述,写得含糊模型就会该用时不用、不该用时乱用、传错参数
  6. 工具数量不是越多越好,挂 30 个工具模型每步都要在 30 个里挑、选错概率明显上升,只挂这个 Agent 真正需要的、功能相近的合并
  7. 工具执行失败时绝不能直接抛异常让 Agent 崩掉,要把错误翻译成模型读得懂的结构化 Observation,模型才有机会自己换参数重试
  8. 对话历史会"滚雪球",模型没有记忆全靠这份历史决策,但它很容易撑爆上下文窗口,必须裁剪——保住任务的根、旧历史压成摘要
  9. Agent 循环必须设 max_steps 上限,模型可能陷入死循环反复调同一工具,没有上限的 Agent 一个 bug 就能在你没察觉时烧掉一大笔钱
  10. 别什么都做成 Agent,流程固定的任务(先查 A 再查 B 拼起来)直接写确定代码又快又稳又便宜,硬交给模型每步现想是用昂贵的不确定性解决不存在的问题

总结

这一趟把 Agent 彻底理清的过程,纠正了我一个特别根本、也特别隐蔽的误解——我一直以为,把一个 AI 助手做得"更强",方向是去找一个【更聪明的模型】。我那版只会聊天的助手答不了"查磁盘""汇总周报",我下意识的反应,是觉得"模型还不够强,等它再强一点就行了"。可这趟梳理让我明白:不是模型不够强,是我【把整件事都压在了模型一个人身上】。一个只会生成文字的模型,无论多强,它也永远没法亲手去读一个文件、调一个接口——这不是"强弱"问题,是"能不能"问题。Agent 这个范式真正教给我的,根本不是"怎么让模型更强",而是【怎么不再要求模型独自扛下所有事】。它把一个任务,清清楚楚地切成了两半:一半叫"决策"——该用哪个工具、参数传什么、下一步往哪走,这个交给模型,因为这是它擅长的;另一半叫"执行"——真正去把那个工具跑起来、去碰那个真实的外部世界,这个【牢牢攥在我自己的程序手里】,因为这是模型永远做不到、也不该让它做的。模型只"出主意",程序负责"动手"——这条边界一旦划清,我那个卡了很久的难题豁然开朗:我不需要一个"无所不能的模型",我只需要一个"会做决策的模型",外加一组"我自己写的、确定可靠的工具"。我忽然意识到,这个"分工"的思路,我其实早就见过。RAG 是同一个思路:模型不擅长记住海量、实时的知识,那就别让它记,把知识放在外部的检索系统里,模型只负责"基于检索回来的内容作答"。Function Calling 也是同一个思路:模型不擅长精确计算和调用接口,那就别让它算,它只负责"决定该调什么",真正的调用交给程序。它们和 Agent,讲的是同一件事:【想清楚哪些事该交给模型,哪些事该留在模型之外】。模型有它锋利的地方——理解意图、做判断、随机应变;也有它根本性的短板——没有记忆、不能动手、结果不确定、还很贵。一个好的 AI 系统,不是把所有事都扔给模型然后祈祷它够强;而是冷静地把任务拆开,让模型只做它最擅长的那部分"思考与决策",把"记忆""执行""确定性的流程"这些它的短板,统统用模型之外的、可靠的工程手段接住。这趟梳理给我的最终启发,早已超出了 Agent 本身:面对任何一个想用 AI 解决的问题,我都会先停下来,做同一道题——这件事里,到底哪一部分是【非模型不可】的判断与创造,哪一部分是【本就该用确定的代码】去稳稳兜住的执行?把这条边界划对,剩下的,无非是把模型这个"会出主意的大脑",和我自己写的"靠得住的手脚",老老实实地接到一起。AI 工程的功夫,十之八九不在那个模型上,而在这条边界划得准不准。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Docker 镜像优化完全指南:1.4GB 镜像是如何瘦到 80MB 的

2026-5-21 13:16:08

技术教程

Java 线程池完全指南:从一次线上 OOM 看懂七个核心参数怎么配

2026-5-21 13:36:36

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索