首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >手写一遍 Multi-Agent,我才看清框架到底藏了什么

手写一遍 Multi-Agent,我才看清框架到底藏了什么

作者头像
烟雨平生
发布2026-07-21 13:26:25
发布2026-07-21 13:26:25
1860
举报

用过 LangChain 的人很多,能讲清 agent.run() 内部在做什么的人很少。

我也是前者。所以这次把框架放一边,从零手写了一个 Multi-Agent 客服系统——能查订单、答 FAQ、处理投诉,多个 Agent 协作。

不是为了取代框架,是想搞懂。

写完不到 600 行、6 个核心文件,最大的收获不是代码,是终于能回答那个问题:Agent 到底在干什么?

代码在这里:https://github.com/helloworldtang/my-multi-agent-system。下面是我拆出来的东西。

一、"Multi-Agent"这个词,是障眼法

先祛魅。

抛开术语,一个 Multi-Agent 系统就四件事:

  1. 调 LLM(带超时、重试、日志)
  2. 工具调用(让 LLM 能调你的函数)
  3. ReAct 循环(LLM 边想边调工具,直到给出答案)
  4. 多 Agent 编排(谁干啥、并行还是串行)

每一件单独看都不复杂。框架的价值在于把它们打包、藏起来、给你一个 agent.run()

但"藏起来"恰恰是问题:出了 bug 不知道在哪一层;想换个模型、改个工具,得先学框架的抽象;面试被问"讲讲你们的 Agent 架构",只能背 API。

所以我决定一件件自己写。

下面按这个顺序拆。

二、function calling:不是 AI 的新能力,是一份协议

很多人觉得 function calling 是"AI 学会了调函数"。

不是。它是一份协议约定:你告诉模型"有这些工具、参数长这样"(JSON schema),模型回复"我要调这个、参数是这些"(JSON),你负责真的执行。

模型从来没碰过你的函数,是你的代码在派发。

理解这一点,工具注册就好写了——本质是把 Python 函数签名翻译成 JSON schema。我用一个装饰器自动完成:

@tool def query_order(order_id: str) -> str: """查询订单状态与物流。 Args: order_id: 订单号,如 ORD20240001。 """ ...

装饰器用 inspect 反射签名 + pydantic 校验参数,自动生成 OpenAI 的 schema。模型返回 {"order_id": "ORD20240001"},pydantic 校验,不合法就回灌让它重试。

整个 function calling 机制,几十行就讲透了。它不是魔法,是"JSON schema + 派发"。

三、ReAct:所谓"自主决策",就是一个 while 循环

ReAct(Reason + Act)被讲得很玄。其实流程朴素得有点扫兴:

模型看问题 → 要调工具?

  • 要:执行工具,把结果喂回去,再问一次
  • 不要:输出最终答案

写成代码,核心就是一个循环加一个停止条件:

for step in range(max_steps): resp = llm.chat(conv.to_dicts(), tools=tool_schemas) if not resp.tool_calls: return resp.content # 模型不再要工具 → 最终答案 for tc in resp.tool_calls: result = execute_tool(tc) conv.tool(result, tool_call_id=tc.id, name=tc.name) # 结果回灌

但这里有个反直觉的点:ReAct 的难点不在循环,在防御。

模型会卡死(连续调同一个工具)、会幻觉参数、会无视你的确认要求。真正花心思的是这些:

  • max_steps 硬上限,防死循环
  • 连续 3 次相同调用 → 判卡死,强制停止
  • 工具执行报错不崩,把错误回灌,让模型换法子
  • 写操作(取消订单、退款)打 requires_confirmation 标记,先问用户

这些才是工程。框架替你做了,所以你以为 ReAct 很简单。

而且——这些不是 ad-hoc 补丁。先记住它们,第六节会收回这条线:它们其实是一种更普适思路的具体实现,叫给不可靠的 LLM 套一层审视

四、多 Agent:最容易过度设计的地方

"Multi-Agent"最容易让人想多。我一开始想上 Actor 模型——每个 Agent 一个消息邮箱,搞订阅、序列化、生命周期。

写了两小时,停下来想了想拓扑,发现自己犯傻。

我的系统是静态 DAG:用户问题 → 路由识别意图 → 挑对应 Agent → 合并回复。这张图是固定的,不需要 Actor 那套动态订阅。函数式 fan-out 两个函数就够:

def fanout(agents, msg): with ThreadPoolExecutor() as ex: return [f.result() for f in [ex.submit(a.run, msg) for a in agents]] def merge(results, llm): if len(results) == 1: return results[0].content # 单 Agent:零 LLM return llm.chat(...).content # 多 Agent:一次摘要去重

Actor 要 3 倍代码,收益为零。这不是偷懒,是拓扑分析后的工程判断——我把这个否决理由写进了设计文档,因为"为什么不用 X"往往比"用了 X"更有信息量。

真正用上多 Agent 的地方是路由支持多意图。用户说"查下 ORD001,顺便问退货政策"——既是订单又是 FAQ。老项目 if/elif 只能选一个,必然漏处理。这里返回多意图,fan-out 并行两个 Agent,再 merge。

这才是 Multi-Agent 协作的真实形态:不是一堆 Agent 互发消息,是一次请求里的并行分工

五、实测:function calling 比 prompt 强,强多少?

光讲道理不算数。我跑了 40 个 prompt × 3 种"让模型调工具"的方式,120 次真实调用:

方式

总成功率

FAQ 场景

原生 tools 参数

95%

87%

response_format=json_object

95%

87%

纯 prompt 引导("输出 TOOL: 名字")

72%

33%

这张表回答了一个真问题:function calling 到底比 prompt 强在哪、强多少?

强在可靠性。原生协议和 json_mode 打平(95%),因为它们都给了模型一个结构化的契约去遵守。纯 prompt 引导在 FAQ 场景只有 33%——模型压根不按你规定的格式,随手回你一段自然语言。

结论很直接:function calling 不是花架子。能用原生协议,就别用 prompt 硬模拟。

剩下 5% 的失败更有意思——不是协议不稳,是模型"觉得自己知道答案,跳过了检索工具"直接回答。这是 RAG 的老难题,得靠 prompt 强制检索解决,跟换不换调用方式无关。

六、LLM 只是跑出结果:你还需要一层审视

写到这,停下来看一个贯穿始终、却很容易被"Agent 很酷"盖过去的事实:

实测里那 5% 的失败、72% 的 prompt fallback,说的是同一件事——LLM 是概率性的。

它会跳过你给的检索工具直接编、会不按你规定的格式输出、会对着一个不存在的订单号一本正经地胡说。"跑出结果"和"结果可靠"之间,隔着不确定性。

这是 LLM 和传统确定性开发的根本边界。你写 if x > 0:,它永远只在 x>0 时进分支;你让 LLM 判断意图,它有概率判错。你不能像信任 if/else 那样,信任 LLM 的任何一次单点输出。

所以一个能上生产的 Agent 系统,光靠 LLM 不够,外面必须套一层审视机制。两种形态:

on-the-loop——系统在回路上监督。 不信任单次输出,用确定性代码约束它:max_steps 防死循环、连续调用检测防卡死、JSON schema 校验防乱编参数、工具报错回灌让它重试、路由置信度低于阈值就降级为"请您描述清楚一些"。LLM 每走一步,都被一道闸门拦一下。

in-the-loop——人在回路里把关。 高风险动作绝不自动执行:取消订单、发起退款这些有现实后果的操作,打 requires_confirmation,先回"确认取消 ORD001 吗?",用户点头才放行。把人插进回路,用人的确定性兜 LLM 的不确定性。

这套"把不可靠的 LLM 单元套进一层驾驭"的思路有个名字:Harness 架构——上下文管理 + 评估循环 + 护栏,目的就一个,让 AI 可靠地完成任务。Claude Code、Cursor 这些工程化做得好的 Agent,内核都是 harness;它们的"好用",很大程度来自 harness 做得厚,不是模型本身多神。

回头看第三节那些"防御":max_steps、循环检测、错误回灌、requires_confirmation——它们不是临时补丁,正是 harness 的具体实现。on-the-loop 是 max_steps 和校验,in-the-loop 是 requires_confirmation。每写一层 LLM 调用,你都会本能冒出一句"它要是出错怎么办",然后加一道审视。手写一遍,这句话会问到你的肌肉记忆里。

而且这些审视,现在还是散落在各模块里的——agent.py 管循环、tools.py 管校验、system 管路由阈值。当它们被收敛成一个统一管上下文、评估循环和护栏的运行时层,就是 harness runtime。Claude Code、Cursor 好用的秘密,LangChain 真正在卖的东西,很大程度就是这一层——而这层该长什么样,手写一遍你就知道了。

这也是为什么"Multi-Agent 到底比单 Agent 强在哪"的答案,不在 Agent 数量,而在你愿意为不可靠性配多厚的审视层

七、写完之后

手写一遍,我得到了什么?

不是"以后可以不用框架了"。框架依然有价值——省事、有生态、有社区。

而是用框架时,终于知道每一行在干什么agent.run() 卡住,我知道去查哪一层;想加个工具,我知道在哪注册;面试被问架构,我能讲清"为什么这么设计"。

还有一条更重要的:懂了 LLM 的边界在哪——哪些可以交给概率,哪些必须用确定性兜底。这条边界,不亲手写一遍,看不见。

"会用"和"懂"之间,隔着一条手写的沟。框架帮你跳过了实现,但没帮你跳过理解。

这条沟,得自己填。

完整代码:https://github.com/helloworldtang/my-multi-agent-system

MIT,有 CI(ruff + mypy strict + 96% 覆盖率门槛),附 DeepSeek tool calling 实测脚本。

uv sync 一键起,感兴趣的可以 clone 下来拆着玩。

Agent设计模式全景图——从ReAct到Multi-Agent的完整知识体系

Agent设计模式(2):ReAct模式深度解析——从原理到实战

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-19,如有侵权请联系 cloudcommunity@tencent.com 删除
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档