Part 1 心智模型:Agent 到底是什么

墨叔:老哥,地基得先打牢。这一 Part 不教你调任何一个库,但这一 Part 的每个词,后面八章都会反复用到。你把它们嚼碎了,后面就是顺水推舟。


1.1 从”补全文本”到”调用工具”:一次认知跃迁

老哥,你先回想一下,你第一次用 ChatGPT 时,脑子里是不是觉得”这玩意儿就是个高级搜索引擎 + 写作助手”?问它问题,它吐答案;让它写诗,它写诗。整个交互是一问一答、说完拉倒。

这种形态,行内叫 completion(补全)模式:你给一段提示,模型补一段回复,对话结束。它和”在搜索引擎里搜完关掉标签页”没有本质区别——信息进去了,结果出来了,模型对环境没有任何持久影响。

那么,“调用工具”这一步,为什么是一道分水岭?

想象你让一个只会上网搜索、但手被绑着不能操作电脑的秘书,去”帮我订明天去上海的机票”。他能在网上查到航班,能告诉你”东航 MU51XX 早上 8 点飞”,但他订不了——因为他没有”下单”这只手。你说”那你帮我订”,他只能回你一段操作指南。

这就是 completion 模式的死穴:模型能”说出”动作,但不能”做出”动作。它生活在纯文本的真空里。

而 agent 的关键一跃,就是给模型接上”手”——让它的输出不再只是给人看的文字,而能变成对外部世界的一次真实调用:查数据库、读文件、发 HTTP 请求、执行命令。模型从”嘴替”变成了”能动手的调度员”。

技术上这一步叫 Function Calling(函数调用),或者更泛化的说法 Tool Use(工具使用)。机制其实朴素得让你意外:你在每次请求里,除了对话内容,还附带一份”工具清单”——每个工具写清楚名字、干什么、需要什么参数。模型不再只输出自然语言,而是可以输出一段**结构化的”我要调用某个工具,参数是这个”**的指令。你的循环拿到这段指令,真去执行那个函数,把结果再塞回给模型,它接着往下想。

老哥,你品一品这个变化的分量:模型第一次能”改变现实”了。之前它说”你应该删这个文件”,只是建议;现在它能真的触发删除(当然,得你循环愿意执行)。这一跃,让 LLM 从”信息处理器”变成了”行动者”。agent 这个词,就是从这里开始有意义的。

我给你个落到你本行的连接。你写 Go 服务时,handler 里调 db.Query(...) 那一刻,程序就从”算”变成了”动”。模型原本只有”算”(生成 token),Function Calling 相当于给它配了一个 db.Query 的入口——只不过这个入口不是你写死的,是模型按上下文临时决定调不调、调哪个、传什么。你早就在用”逻辑决定调哪个函数”这套思维,区别只是:以前是 switch 决定,现在是模型”猜”决定,而你的循环负责把”猜”安全地落地。

【思考题】

  1. “能说出动作”和”能做出动作”,中间差的那个东西,在 agent 架构里具体是谁提供的?是模型还是你的代码?
  2. 如果 Function Calling 只是让模型输出”我要调函数 X 参数是 Y”,那”真正执行 X”这件事,是谁的责任?为什么不能让模型自己执行?
  3. 你用过的最接近”工具调用”的编程概念是什么?(提示:想想你代码里”根据配置/输入决定调用哪个 handler 或 plugin”的模式)

【参考答案】

  1. 差的那层是执行环境,由你的代码循环提供。模型只负责产出”调用意图”(结构化指令),而”真正去查数据库 / 读文件 / 发请求”是由循环里的代码完成的。换句话说,模型给了”动词 + 宾语”,你的代码负责”把这句中文翻译成实际操作并承担后果”。这也是为什么 agent 的可靠性不取决于模型多聪明,而取决于这个执行环境设计得多严谨。
  2. “真正执行”是循环(你的代码)的责任,绝不能让模型自己执行。原因有三:一是模型运行在推理服务里,根本没有你本地/服务器的文件系统、数据库、网络的访问权限;二是如果让模型直接执行,等于把”删库”这种危险动作的开关交给了概率系统,毫无护栏;三是执行结果(成功/报错/输出)必须被读回循环、再喂给模型形成闭环,而模型自身没有”读回结果”的能力。所以永远是:模型提议 → 代码执行 → 代码读回结果 → 模型继续。
  3. 最贴近的是你后端里的 策略模式 / 插件机制 / 路由表:比如根据请求 path 或消息类型,分发到不同的 handler;或者一个中间件 pipeline,按配置决定启用哪些插件。区别仅在于”决策源”:传统做法是代码里的 if/switch/配置 决定走哪个分支,agent 是把”决策”外包给了一个会读上下文的语言模型。你一旦接受”模型只是个更灵活的路由决策者”,agent 就一点都不神秘了。

小结:completion 模式里模型只能”说”,agent 模式给它接上”手”(Function Calling),让输出能变成真实动作。这一步让 LLM 从信息处理器变成行动者,而”手”和”执行后果”永远由你写的循环兜底。


1.2 ReAct 拆解:Reasoning 与 Acting 为什么必须交替

老哥,你听过 ReAct 这名字对吧?很多教程把它讲得像什么高深范式,其实它的核心就八个字:边想边做,做完再想。拆开就是 Reason(推理)和 Act(行动)两个动作,交替进行。

我先把这个”交替”为什么必要,给你讲透。

假设你让 agent 干一件稍微复杂点的事:“把我项目里所有用了过期 API 的函数找出来,列个清单。” 如果模型只推理不行动——也就是只在脑子里想、不调任何工具——它能给你什么?它只能凭训练记忆瞎猜几个函数名,十有八九是编的(幻觉)。因为它根本没读过你的代码库,它”想”出来的东西没有事实支撑。

反过来,如果模型只行动不推理——也就是一上来就狂调工具、却从不停下来归纳——它会像个无头苍蝇:读这个文件、读那个文件,却不把读到的信息串成”哪里过期了”的结论,最后淹没在工具返回的海量文本里。

ReAct 的精髓,就是让这两件事咬着牙交替:先 Reasoning 一下”我目前知道什么、下一步该查什么”,再 Act 去查;拿到结果后,再 Reasoning”这个结果说明什么、还差什么”,再 Act……如此循环,直到 Reasoning 得出结论”够了,可以回答了”。

你看,这其实复活了我们在开篇讲的那张图:Reasoning 是模型在纸条上写”我分析当前状况,下一步建议做 X”,Act 是循环真的去执行 X 并读回结果。ReAct 只是把这个节奏正式化了。

我给你一个生活化的场景。你装修房子,工长(模型)不会一上来就砸墙(只行动),也不会坐在办公室纯想三个月不出门(只推理)。他是:去现场看一眼(Act: 观察)→ 回来跟你说”墙面空鼓,得铲掉重做”(Reason)→ 安排人铲(Act)→ 再看铲完的底子(Act: 观察)→ “底子没问题,可以刷漆了”(Reason)。观察和思考交替,才把一件事真正办成。只思考不看的工长给你的是纸上蓝图;只干不想的工长给你的是拆错的墙。

为什么这个交替对 agent 特别重要?因为模型的”推理”需要事实喂养,而事实只能靠”行动”获取。单轮推理的信息上限,就是你一次性塞进提示词的那些;而 agent 通过反复行动,能不断把新事实(文件内容、查询结果、报错)喂回推理,让结论越来越靠谱。这本质上是在用”时间换准确性”——多转几圈,比一次瞎猜强。

【思考题】

  1. 在你自己的开发经历里,有没有”先想后做、做了再看、看了再想”的例子?和 ReAct 对照一下。
  2. 如果某个任务”不需要查任何外部信息”(比如纯翻译一段文字),ReAct 还有必要吗?为什么?
  3. 模型在 Reason 阶段”想错了”,但 Act 拿到的真实结果又把它纠正了——这个过程叫什么?它为什么比一次生成更可靠?

【参考答案】

  1. 太有了。比如你调一个线上 bug:先看监控和日志(Act: 观察)→ 推理”大概是数据库连接池打满”(Reason)→ 去查连接池配置和慢查询(Act)→ 看到某条 SQL 没走索引(Act 观察结果)→ 推理”就是它,加索引”(Reason)→ 改代码上线验证(Act)。你本能就在跑 ReAct,只是决策人是你。agent 不过是把”你”换成了模型+循环。你早就懂这个节奏,现在只是把它形式化、自动化。
  2. 没必要。ReAct 的价值全在”行动能带来推理没有的新事实”。纯翻译任务,模型一次补全就能给出结果,中间插工具调用纯属多余,反而增加延迟和出错面。这提醒你一个设计原则:任务越封闭、越不需要外部事实,越不该上 agent。这也是后面会讲的”反模式”之一——拿大炮打蚊子。
  3. 这个过程叫基于环境的纠错(grounding / self-correction via environment)。它更可靠,是因为”真实世界的反馈”是硬约束:模型可以胡思乱想,但工具返回的是客观事实(文件里确实没有那个函数、命令确实报了错)。错误推理一碰到硬事实就被打脸,循环把硬事实再喂回去,模型的下一轮推理就被拉回地面。这比”模型自己检查自己写的答案”可靠得多——后者是让瞎子验收瞎子,而前者是让事实来验收。

小结:ReAct = Reason 与 Act 交替。推理需要行动获取的事实来喂养,行动需要推理来定向。交替节奏是 agent 在”时间换准确性”上胜过单次生成的根本原因。


1.3 The Agent Loop:最简循环的代码骨架

老哥,前面讲了这么多”递纸条""交替”,现在我把这个循环用 Python 示意写出来。你盯住它看三十秒,会发现——这玩意儿跟你写过的任何事件循环没本质区别。

# 最简 Agent Loop(示意,非可运行工程)
from typing import List, Dict
 
def agent_loop(goal: str, tools: Dict[str, callable], max_steps: int = 20):
    # 1. 初始化"纸条":系统提示 + 用户目标
    messages = [
        {"role": "system", "content": "你是一个助手,可用工具见下方,按需调用直到完成目标。"},
        {"role": "user", "content": goal},
    ]
 
    for step in range(max_steps):
        # 2. 把纸条递给"大厨":模型决定下一步
        resp = model_chat(messages, tools=tools)   # 模型返回:要么直接回答,要么"我要调工具X,参数是Y"
 
        if resp.is_final_answer:                    # 3a. 模型说"我想清楚了,这是答案"
            return resp.text
 
        # 3b. 模型说"我要调工具" → 循环负责真的去执行
        tool_name = resp.tool_call.name
        tool_args = resp.tool_call.args
        try:
            result = tools[tool_name](**tool_args)  # 代码执行,不是模型执行!
        except Exception as e:
            result = f"工具执行出错: {e}"            # 4. 错误也读回纸条,让模型自救
 
        # 5. 把"动作 + 结果"写回纸条,进入下一轮
        messages.append({"role": "assistant", "content": resp.raw})
        messages.append({"role": "tool", "content": f"[{tool_name}] 返回: {result}"})
 
    return "超过最大步数,未完成"                     # 6. 兜底:防止无限转圈

老哥,你逐行看,告诉我哪一行是”魔法”?没有吧。for 循环、if 分支、字典查函数、try/except——全是你在 Go 里写了无数遍的东西。唯一的”陌生”是 model_chat(messages, tools=tools) 那一行,它把”下一步做什么”的决策,从你写死的 switch,换成了一个会读上下文的模型。

我帮你把这个骨架翻译成你熟悉的 Go 心智:

  • messages 就是那张不断变长的纸条,每一轮都包含历史(系统提示、用户目标、之前的思考、工具调用、工具结果)。模型是无状态的,它每次都从头读这张纸条来决策——所以纸条写得好不好,直接决定模型靠不靠谱。
  • resp.is_final_answer 对应你代码里的”结束条件”。agent 必须有一个明确的”我干完了”信号,否则 for 会一直转。这是工程里特别容易忘的一点:没有终止条件的循环是个炸弹。
  • tools[tool_name](**tool_args) 就是 handler 分发。模型只是选了 handler 名和参数,真正执行的是你的代码——和 HTTP 路由分发一模一样。
  • try/except 把工具报错也塞回纸条。这一点极关键:模型第一次调错工具,看到报错后能自我纠正重试。这就是上一节说的”基于环境的纠错”。

你现在应该彻底信了:agent 的”智能”不在模型独占,而在这个循环把模型、工具、上下文、错误兜底串成一个会自我修正的引擎。模型是发动机,循环是底盘和变速箱。光有发动机,车跑不起来;底盘烂,发动机再猛也翻车。

【思考题】

  1. 上面这段代码里,如果去掉 messages.append(... tool 返回 ...) 这行,会发生什么?为什么这是致命的?
  2. max_steps 这个兜底为什么必须有?你能想到没有它会导致什么事故吗?
  3. 把这段 Python 翻译成你脑子里对应的 Go 结构:messages 像什么?tools 字典像什么?model_chat 像什么?

【参考答案】

  1. 去掉那行,模型就永远看不到工具返回了什么。下一轮它还是基于”我没收到结果”的旧上下文决策,于是会重复调同一个工具、或者瞎编一个结果。这正对应开篇”递纸条的人少写了锅里的状态”——循环没把事实读回纸条,模型的推理就悬空了。这是 agent 最常见的低级 bug 之一:工具调了,结果丢了。记住:工具结果必须回流进上下文,闭环才成立。
  2. max_steps 是防”无限循环”的保险丝。没有它,万一模型陷入”调工具 A → 看结果 → 又想调 A”的死循环,或者永远不返回 is_final_answer,你的服务会卡死、token 费用疯涨、甚至工具被反复调用造成副作用(比如重复发请求)。任何 agent 循环都必须有步数上限 / 时间上限 / 成本上限三重兜底之一。这是”确定性程序”对”概率程序”最基本的约束——你不能完全信任模型知道”什么时候该停”。
  3. 翻译过来:messages 像一个不断追加的审计日志 / 事件溯源(event log),记录了一问一答一动作的全过程,模型每次都重放整段日志来决策;tools 字典像一个 handler 路由表 / 依赖注入的 service map,key 是名字,value 是可执行函数;model_chat 像一个外部决策服务(RPC 调用),你传入上下文,它返回”下一步建议”。你看,全是后端熟透了的概念,agent 只是把它们重新组合,让”决策”那一步变成概率性的。

小结:最简 Agent Loop 就是”观察→模型决策→代码执行工具→结果回流→再决策”的 for 循环。它的每一行都是你熟悉的后端概念,唯一的陌生处是”决策源变成了模型”。循环的结构(终止条件、结果回流、错误兜底)才是可靠性的真正来源。


1.4 Context Window = Agent 的”工作台记忆”

老哥,这一节聊一个你很快就会撞上的硬限制:上下文窗口(Context Window)。你把它理解成 agent 的”工作台”——模型每次决策时,只能看到摊在这张台子上的东西。台子有多大,模型的世界就有多大。

我们复习一下:模型是无状态的。它不记得上一轮,不记得你是谁,不记得昨天聊了啥——除非你把那些东西全写进当前这一次的输入里。而”输入能塞多少”,就是这个上下文窗口的大小。早期模型是 4k、8k token(大概几千字),现在能到 128k、200k 甚至更多,但再大也是个有限的天花板。

为什么这事儿对 agent 是命门?因为 agent 干活要塞进窗口的东西太多了:系统提示(告诉它你是谁、能调什么工具)、用户目标、历史对话、每一次工具调用的请求和返回、模型的思考过程……尤其工具返回,动辄几百上千字(比如读一个文件、查一次数据库)。转个十几圈,窗口就满了。满了之后怎么办?模型要么开始忘掉最早的内容(截断),要么直接报错罢工。

这带来一个你作为后端特别该警觉的直觉:agent 的”记忆”不是数据库,是正在燃烧的纸。每多转一圈、每多调一次工具,工作台就更挤一分,而且旧的会被新的挤掉。这和你习惯的”内存 + 磁盘”模型完全不同——在你那儿,数据放内存不够就落盘,永远在;在 agent 这儿,上下文窗口是唯一的”工作台”,没有自动落盘,挤掉就没了。

我给你个比喻。模型像个只有一张办公桌的会计,所有要算的账、要查的单子、老板的指令,都得摊在这张桌上。桌子小,单子多,他只能把最底下那叠先挪开(遗忘)才能腾地方。你想让他”记得三个月前那笔账”?对不起,那叠早被挪走了,除非你主动把那笔账的摘要再贴回桌上。

所以”上下文工程(Context Engineering)“这门手艺,本质就是:在有限的桌面上,永远摆对最重要的东西。哪些必须留(用户目标、当前任务状态、关键约束),哪些可以丢(已经用过的、冗余的工具返回),哪些该压缩(把长对话总结成三句话再放回去)。这会是后面 Part 4 和 Part 8 的重点。

给你一个落到实处的连接。你写 Go 时,context.Context 也是个”贯穿调用链的上下文载体”——但它能无限传、能挂值、能取消。agent 的上下文窗口不能无限,而且装太多反而变傻(注意力被稀释)。所以你不能像传 context.Context 那样无脑把历史全塞进去,得做选择性裁剪。这是后端直觉会坑你的地方:你以为”上下文越多越全”,其实”上下文越精越准”。

【思考题】

  1. 模型”忘掉最早内容”这件事,对你设计 agent 的工具返回有什么启示?(提示:工具返回该多长?)
  2. 你代码里的 context.Context 和 agent 的上下文窗口,本质区别在哪?为什么不能类比着用?
  3. 如果窗口有限,而任务需要”永远记得用户一开始定的目标”,你有什么朴素的处理思路?(先别想复杂方案,给直觉)

【参考答案】

  1. 启示是:工具返回必须做截断和提炼。你绝不能把一个 5000 行的文件原样塞回窗口——那一下就吃掉大半张桌子,还稀释模型注意力。正确做法是工具层就做裁剪:只读相关片段、只返回摘要、超过阈值就截断并标注”已截断,需要时可再查”。工具返回是”往工作台上堆东西”的动作,你得像个收纳狂一样控制每件东西的体积。这对应后面”上下文管理”里的滑动窗口、摘要压缩等手法。
  2. 本质区别在容量与代价:context.Context 在你程序里几乎是”无限且廉价”的载体,挂多少值不影响正确性,只是内存占用;而 agent 的上下文窗口是稀缺且昂贵的(token 计费、越多越慢、越多越容易失焦)。更致命的是,Context 是确定性的数据结构,而上下文窗口里的文本会被模型”注意力”处理——塞太多无关内容,模型反而抓不住重点(这叫”lost in the middle”现象:中间的内容容易被忽略)。所以不能把两者类比成同一个东西,agent 的上下文需要主动的”编辑”而非无脑”传递”。
  3. 最朴素的思路就两条:一是把目标反复重申——每轮都把它放在窗口醒目位置(比如系统提示固定段),或者每轮开头贴一句”当前总目标:XXX”;二是用外部存储兜底——把目标写进一个不会丢的地方(文件 / 数据库 / 一条固定记忆),窗口满了就从那里重新读。这两条合起来就是后面 Part 4 要讲的”短期记忆(窗口)“和”长期记忆(外部存储)“的分工。你现在给的直觉,已经摸到了门边。

小结:上下文窗口是 agent 唯一的”工作台”,有限且昂贵,不像你熟悉的 context.Context 能无脑传。工具返回要裁剪,关键状态要反复重申或外置——这门”在有限桌面摆对东西”的手艺,叫上下文工程。


1.5 确定性程序 vs 概率程序:你该怎么信任它

老哥,这一节是 Part 1 的收口,也是后面所有工程决策的总纲。一句话:传统程序是确定性的,agent 是概率性的,而你得学会在”不必然正确”的前提下,依然造出靠谱的系统。

你写了五年 Go,你的世界观是确定性的:a == b 就是真,db.Insert 成功就进库了,同一个输入永远同一个输出。你信任程序,是因为它的行为是可预测、可复现、可证明的。

但 agent 不是。同样的输入,问两遍,模型可能给出不同路径;模型可能”觉得”该调工具 A,其实该调 B;它生成的代码可能编译不过。它的每一步都是从一个概率分布里采样,不是从一个确定性函数里算出来。这就像你雇了个才华横溢但偶尔走神的同事——大多数时候对,但保不齐哪次就懵了。

那问题来了:你该怎么信任一个”保不齐会懵”的东西?

墨叔给你的第一条心法:别信任模型的”输出”,信任你设计的”流程”。模型输出的具体内容是不可信的,但你设计的流程——终止条件、工具校验、错误重试、人类确认——是确定性、可信的。把”必须正确”的事,从模型的嘴里,挪到你代码的手里。

第二条心法:用确定性去框住概率性。具体手法后面会展开,但这里先给你一张清单的雏形:

  • 结构化校验:模型返回的工具调用,先 schema 校验,字段不对就打回重来,别直接执行。
  • 危险动作隔离:删库、发钱、外发邮件这类,必须走确认 / 沙箱 / 白名单,模型只有”请求权”。
  • 结果可验证:工具执行后,用确定性代码检查结果是否符合预期(比如”文件是否真的创建了”),而不是问模型”你成功了吗”。
  • 人类兜底(HITL):高风险节点暂停,等人拍板。

第三条心法,也是最重要的:接受”大概率对”,而不是追求”必然对”,但给失败留好退路。你不会让 agent 直接在生产库执行破坏性 SQL,但你会让它先生成、再给人审、再执行——把”概率”关在”人审”这个确定性闸门后面。这是一种新的工程美学:在不确定性之上,用结构搭出确定性。

最后给你一个连接,收束整个 Part 1。你当年从”写脚本”走到”写服务”,学会的是”如何处理确定世界里的异常”;现在从”服务”走到”agent”,要学的是”如何在概率世界里搭确定性骨架”。前者你已经是老手,后者只是换了一套异常来源——异常的来源从”网络超时、空指针”变成了”模型瞎指挥、幻觉、采样偏差”。你处理异常的功力,直接决定了 agent 的靠谱程度。 这,就是为什么我说你不是不会,你只是还没把那套功力平移过来。

【思考题】

  1. 你代码里”可复现”这个特性,在 agent 上为什么很难保证?这给你做测试带来什么新麻烦?
  2. “信任流程不信任输出”——套用你后端经验,流程里的哪些”确定性闸门”是你早就用惯的?(比如事务、校验、幂等)
  3. 如果老板问你”这个 agent 靠谱吗”,你不能说”模型很聪明”,你该怎么用工程语言回答?

【参考答案】

  1. agent 难复现,因为模型的采样有随机性(即使 temperature 设 0,长链路里多步决策的组合也会漂移),而且工具依赖的外部状态(文件、网络、时间)本身就在变。这给测试带来的新麻烦是:你不能写”输入 X 必然得到 Y”的单元测试,而得写”输入 X 在大多数情况下走向预期结果、且失败时不会造成破坏”的概率性 / 轨迹级评估。也就是后面 Part 9 要讲的 eval——用一大批任务跑成功率,而不是单测断言。这是确定世界测试思维的一次重要升级。
  2. 你早用惯的确定性闸门,平移过来全是宝贝:
    • 事务(transaction) → agent 的”危险动作要么全成要么全滚”,比如改多个文件要有回滚点;
    • 入参校验(validation) → 模型返回的工具调用先 schema 校验再执行;
    • 幂等(idempotency) → 工具重试时不能重复副作用(比如”发送”类操作要带去重键);
    • 熔断 / 超时 → 模型或工具卡死要有上限。 你看,agent 工程不是发明新东西,是把你后端那套”防御性编程”重新用到了”决策源变成概率”的新场景里。
  3. 工程语言该这么答:“它的核心决策由模型给出,单次输出不保证可复现;但我们在流程层做了结构化校验、危险动作隔离、结果可验证和人类确认四道闸门,把破坏性风险控制在可接受范围。我们用一批标准任务做了成功率评估,目前关键任务达成率 X%,失败 cases 都有明确兜底不会造成数据损坏。“——把”靠不靠谱”从”模型行不行”重构为”流程兜得住不兜不住”,这才是架构师该有的回答,也恰好是 Part 5/Part 9 的主题。

小结:agent 是概率程序,单次输出不可全信;但你设计的流程(校验、隔离、可验证、人类兜底)是确定性的,足以框住概率。你后端那套防御性编程功力,正是 agent 可靠性的真正来源——你不是不会,只是要把功力平移到”决策源变成概率”的新场景。