哪些 Agent 能力值得自己造

哪些 Agent 能力值得自己造
Asaakii一、一个加功能要动四个文件的版本
我的项目里,最早那版「框架」是我自己都不想维护的东西。
那时候我刚学完怎么从零搭一个 Agent 框架,正是手痒的时候。于是我写了抽象基类,写了插件注册机制,给节点设计了继承体系,一切都很「框架」。然后需求来了:加一条新的业务链。
我得改路由逻辑、改基类、改节点注册、改报告渲染,一共四个文件。加第二条的时候,还是四个文件。
问题不在于我写得不够好,而在于我造错了地方。那些抽象基类和插件机制,解决的是「怎么把代码组织得像个框架」,可我真正要解决的是「怎么让加一条业务链变得便宜」。这是两件事。
后来我把大部分抽象砍掉了,重新想了一遍哪些东西值得我亲手造、哪些根本不该我造。现在加一条业务链的成本是:往一张表里加一条记录,再实现对应节点。路由逻辑一行都不用动。
这篇写的就是这个「砍掉重来」的过程,以及它背后那条我认为最重要的判断标准。
二、先交代这个系统在做什么
后面所有例子都来自同一个项目,先花一段话把它说清楚,不然那些「业务链」「模型族」会一直悬在空中。
这是一个面向县域经济分析的智能体系统。你可以把它想成一个「会写分析报告的助手」:使用者(通常是研究人员或政府部门)提一个问题,比如「A 县今年的产业发展有什么瓶颈」,系统调度一批「专家角色」,有人负责调数据、有人负责跑统计模型、有人负责把结论写成政策建议,最后产出一份带证据的报告。
它不是聊天机器人。它的每一次运行都对应一条相对固定的「工作流程」:先做什么、再做什么、谁来做、做完输出什么、要不要人工复核,都是提前规定好的。这类系统在业内一般叫 workflow 型 Agent(流程编排型),区别于那种让大模型自己天马行空决定下一步的 autonomous 型 Agent(自主决策型)。
这一点很关键:这个系统的大部分「决策」是我用确定性代码写死的,不是交给大模型现场发挥的。整篇文章的设计,几乎都是这个选择的延伸。
三、先把几个词说成人话
这篇会反复出现几个词。初学者最容易卡在这里,我先用最朴素的说法过一遍,后面看到就不慌了。
- 运行时 / 编排 / 领域:一个 Agent 系统从下到上大致分三层。最下面是「怎么跟大模型说话、怎么执行工具」,中间是「多个步骤按什么顺序跑」,最上面是「在我的业务里,什么算合法、该走哪条流程」。下一节有张表专门讲它们。
- 注册表(registry):说白了就是一张清单,或者说一份配置表。与其把「有哪些业务链、每条链怎么跑」写成一堆
if-else散在代码里,不如集中列成一张表,程序去查表办事。加能力就是往表里加一行。 - 契约(contract):一段机器可读的「准入条件」声明。比如「跑聚类模型,至少要 5 个县、2 个指标」,把这句话写成程序能检查的数据结构,就是契约。它规定了够不够格进入下一步。
- 门禁(gate):真正拿契约去卡的那道关。数据不够、章节缺失,就在这里被拦下,不让往下走,也不出正式结论。
- 状态黑板(state):LangGraph 这类编排框架里,各个步骤之间靠一个共享的大字典传递信息,上一步把结果写进去,下一步从里面读。像一块所有人都能写、都能看的黑板。本文里
state就是它。 - trace(痕迹,执行记录):每一步都往一个列表里记一笔「我是谁、干了什么、成没成」。出问题时,顺着这条痕迹就能复盘,不用靠猜。
- 声明式(declarative):只描述「要什么」,不写「怎么一步步做」。注册表和契约都是声明式的:你声明「这条链需要这些输入、派这些角色」,至于程序内部怎么调度,是另一回事。
这几个词是全文的骨架,尤其「注册表 + 契约」这对组合,是我这层框架的全部核心。
四、先把”造框架”说清楚
这篇不是要劝你一开始就做一个框架。对初学者来说,最好的顺序通常是:先用现成 SDK 写通一次调用,再用现成编排框架跑通工作流,最后只在重复痛点出现后再抽象。
前面提到的三个层次,先记住它们各自的职责:
| 层次 | 它解决什么 | 常见例子 |
|---|---|---|
| 运行时层 | 怎么调用模型、传消息、执行工具 | LLM SDK、MCP、模型提供商适配 |
| 编排层 | 多个步骤按什么顺序运行 | LangGraph 的节点、边与状态 |
| 领域层 | 在你的业务里什么算合法、该走哪条流程 | 业务链、数据门槛、人工复核规则 |
一句话记法:运行时管「怎么说话」,编排管「按什么顺序走」,领域管「在我这行什么才算对」。前两层是所有 Agent 系统都长得差不多的部分,第三层才是每个业务各不相同的部分。
后文的项目案例只是在说明:当领域规则已经稳定且重复出现时,为什么我选择在第三层做一层很薄的抽象。
五、教科书里的自建框架长什么样
先把参照系立起来。一个教学向的 Agent 框架,通常会包含这几层。
1. LLM 抽象层。 统一多个提供商(OpenAI、本地 vLLM、Ollama……)的调用差异,甚至能自动探测 provider。
2. Message 类。 统一消息格式,用 Literal 把 role 限死在四个取值上,对内带 timestamp 和 metadata,对外 to_dict() 转成 OpenAI 兼容格式。
1 | MessageRole = Literal["user", "assistant", "system", "tool"] |
3. Config 类。 把硬编码的配置集中起来,支持 from_env() 从环境变量覆盖。
4. Agent 抽象基类。 用 abc 定义统一接口,强制子类实现 run()。
1 | class Agent(ABC): |
5. 范式框架化。 把 ReAct、Reflection、Plan-and-Solve 这几种「Agent 的思考套路」改造成继承同一基类的组件。ReAct 是「边想边做、想一步做一步」,Reflection 是「做完自己回头挑错再改」,Plan-and-Solve 是「先列计划再逐条执行」,都是让大模型更靠谱的常见范式,这里不展开。
6. 一个统一抽象。 比较激进的做法是「万物皆为工具」,把 Memory、RAG、MCP 全都抽象成 Tool,消除多余的概念层。
这套设计的自洽性很好,作为学习路径也非常清晰。自建框架的理由通常有三条:通用框架过度抽象、迭代太快导致 API 不稳、实现黑盒化难以定制。这三条我都认,但它们在我的项目里指向了完全不同的结论。
六、我实际造了什么,没造什么
我的项目底层跑在一个现成的 Agent 运行时上,工作流引擎用 LangGraph。把整个系统按层摊开,界线其实非常清楚:
%%{init: {'flowchart': {'wrappingWidth': 620}}}%%
flowchart TB
L3["✅ 领域层 —— 自己造<br/>业务链注册表 · 角色注册表<br/>模型族:注册 / 路由 / 契约 / 门禁<br/>产物注册表与任务状态机 · 节点契约"]
L2["❌ 编排层 —— 直接用<br/>LangGraph:图编排 / 状态机"]
L1["❌ 运行时层 —— 直接用<br/>LLM 适配与多提供商 · 消息格式与对话循环<br/>工具执行与 MCP 注册 · 持久记忆与上下文管理"]
L3 --> L2 --> L1
规律很直白:凡是通用的我都没造,凡是领域的我都造了。
理由很朴素。我造一个 LLM 适配层,能比现成的好吗?大概率不能,我只会写得更少、bug 更多、还得自己维护。但「一份县域经济分析报告在什么条件下才算合格」这件事,市面上没有任何框架知道。这才是只有我能写的部分。
教科书说「通用框架过度抽象」,这话对;但结论不该是「所以我从零再造一个通用框架」,而应该是:在通用框架之上,造一层贴合我领域的薄抽象。
七、我的框架:注册表 + 契约
那么这层「领域框架」长什么样?核心就两个概念。下面几节我会先给零件,第八节再把一个真实请求从头到尾串一遍,你就能看到它们怎么协作。
7.1 业务链注册表:能力即配置
系统里有近十条业务链(运行监测、产业诊断、瓶颈归因、政策决策、项目招商、模型分析、数据治理……)。它们不是十段 if-else,而是一张注册表里的十条声明:
1 | WORKFLOW_CATALOG: list[dict[str, Any]] = [ |
一条声明就说清了:怎么被触发、需要什么输入、派哪些角色、能用哪些工具、产出什么、要不要人工复核。
关键是查表这一步长什么样。整篇文章的地基就在这几行,路由不做判断,它只是拿用户的问题去表里找命中的那条:
1 | def route_workflow(question: str) -> dict[str, Any]: |
看清楚它做了什么、没做什么:它没有问大模型「这个问题该走哪条链」,只是做了一次确定性的关键词匹配,然后把整条声明 spec 原样带出来。后面的调度、派角色、挂工具,全都读这个 spec,不再有第二处「知道业务链长什么样」的代码。
新增一条业务链,主要工作是往这张表里加一条记录,再实现对应节点,而不是去改路由逻辑。路由逻辑本身一行都不用动,因为它只认这张表。这就是「把『加功能』变成『加数据』」最直接的一次兑现。
7.2 角色注册表:角色是文件,不是代码
系统里的「专家角色」(首席经济学家、政策策划专家、规划专家、报告编辑……)没有一个是 Python 类。它们都是带 frontmatter 的 markdown 文件(frontmatter 就是文件顶部两条 --- 之间那段结构化配置,写博客的人应该很熟):
1 | --- |
上半段的 frontmatter 是给程序看的配置,下半段的正文是给大模型看的提示词。加载器就是几十行:解析 frontmatter,body 当提示词。
1 | def load_role(role_id: str) -> dict[str, Any]: |
于是第 7.1 节里 spec["roles"] 那几个 id,到这里就变成了一份份具体的角色配置:程序读 frontmatter 知道「这个角色能用哪些工具、该产出什么」,大模型读正文知道「我该以什么身份、什么口吻思考」。一份文件同时喂给了代码和模型两边,这是这个设计最舒服的地方。
它还带来几个我做完才体会到的好处:
- 业务专家可以参与维护。调整角色的措辞和边界通常不需要改 Python;但 frontmatter 仍应经过字段校验、评审和版本控制,避免把错误配置直接带进生产流程。
- frontmatter 是机器可读的契约。
business_chains决定它属于哪条链,default_tools限定它能用什么工具,confidence_policy: evidence_required声明了它「必须有证据才能下结论」,这些都能被代码检查。 - 版本控制天然可用。角色的每一次修改都是一次 diff。
有意思的是,这个「markdown + frontmatter」的模式并不是我发明的。现在主流 Agent 工具的技能(skill)和子代理定义,几乎都是这个形状。能力的定义正在从代码往声明式文件迁移,我只是撞上了同一个答案。
7.3 模型族:一个完整的小框架
这是我最花心思的一块,也是最能说明「注册表 + 契约」价值的例子。
系统要支持多种统计/计量模型:综合评价、聚类、回归、因果推断、时序预测、异常检测、空间计量……如果每来一种就写一个节点,很快就会失控。所以我把它做成了一个小框架,六个部件各司其职:
flowchart LR
Q["模型请求"] --> R["router<br/>该用哪个模型族"]
R --> C["catalog<br/>模型族注册表"]
C --> S["input_specs<br/>契约:要什么输入<br/>多少数据才够"]
S --> G{"quality_gate<br/>条件够不够"}
G -->|"够"| E["executor<br/>执行"]
G -->|"不够"| B["blocked / needs_data<br/>不出正式结论"]
E --> P["report<br/>渲染"]
这六个部件里,只有 executor 装着真正的算法。其余五个回答的都是「谁能跑、要什么、够不够、算完怎么呈现」,全是编排与约束,而这恰恰是每加一个新模型族时最容易写重复的部分。
契约长这样,注意 minimum_shape,它把「多少数据才算够」写成了机器可读的声明:
边界说明: 这里的
minimum_shape只是”允许流程进入下一步”的最低工程门槛,不是统计结论已经可靠的证明。特别是因果推断,还需要另行检查识别假设、对照组可比性、样本量与稳健性;不满足时系统应拒绝输出正式因果结论。
1 | MODEL_INPUT_SPECS = { |
门禁就是拿这份契约去卡的那道关,逻辑朴素到几乎不用解释,但它是「确定性代码替大模型把关」的典型:
1 | def quality_gate(family_id: str, dataset_shape: dict) -> dict: |
数据不够,它就返回 needs_data,并说清差在哪个维度、差多少,流程到此为止,不出正式结论。这件事你不能指望大模型自觉,它高兴起来 3 个样本也敢给你讲因果。把关得是代码的事。
路由依然是确定性的关键词匹配,而且这点我很得意:它会把「为什么这么选」一起返回。
1 | def route_model_family(request: dict[str, Any]) -> dict[str, Any]: |
reason 和 matched_terms 这两个字段不参与任何计算,纯粹是为了可解释。出了问题,一眼就能看出是用户指定的、关键词命中的、还是兜底兜出来的。这是我在调试中被坑过之后加的:当路由是个黑盒时,排查成本高得离谱。
现在新增一个模型族要做什么?往 catalog 加一条、往 input_specs 加一条契约、往 ROUTE_TERMS 加一组关键词、在 executor 里实现算法。路由、门禁、报告渲染全都不用动。
7.4 产物注册表与任务状态机
产物(报告、数据集)也是注册表:
1 | ARTIFACT_SPECS: dict[str, dict[str, str]] = { |
任务状态则是从状态黑板推导出来的,而不是让节点各自去设置,也就是保持单一事实来源:
1 | def _derived_status(state) -> str: |
这个小函数避免了一类很讨厌的 bug:某个节点忘了把任务标记成待复核,结果有问题的产出被当成成功的发出去了。「让每个节点自己记得设状态」是一种极易出错的约定,只要有一个节点漏了就出事。现在状态是算出来的,不是设出来的,它只看黑板上的事实(有没有要求复核、复核有没有挑出问题、有没有通过),漏标记这件事从根上没了。
7.5 节点契约
最后是最简单也最重要的一条约定:每个节点都是 state -> state 的普通函数,并且必须往 trace 里记一笔。
1 | def some_node(state: CountyEconomyState) -> CountyEconomyState: |
「节点是 state -> state 的函数」这句话值得多看一眼:它读黑板、往黑板上写、再把黑板交给下一个节点。整条工作流就是一串这样的函数首尾相接,LangGraph 负责按图把它们串起来。没有基类,没有装饰器,没有元编程,契约靠约定和测试保证,不靠继承。
我一开始真的想过写个 BaseNode 抽象基类,后来放弃了。它除了增加一层间接,什么也没带来。节点之间的共性其实只有读状态、写状态、记 trace 三件事,用一个 add_step 辅助函数就够了。
能用函数解决的,就别上类;能用约定解决的,就别上继承。
八、把零件串起来:一个请求从头走到尾
前面拆了一堆零件,初学者最容易在这儿丢掉全局。所以这一节我们只做一件事:拿一个真实问题,跟着它从进到出走一遍,看每个零件在哪一步接手。
假设用户问:「帮我用因果推断评估一下 A 县那项产业补贴政策有没有效果。」
第 1 步,命中业务链。route_workflow(question) 拿这句话去 WORKFLOW_CATALOG 里扫 trigger_phrases,「因果推断」命中了 model_analysis 这条链。返回的 spec 里写着:需要 county_code、period,派 chief_economist / model_analyst / report_editor 三个角色,产出两份产物,且 human_review_policy 要求「进入政策流转前必须人工复核」。到这里,「这次要干什么」已经完全确定,而且没问过大模型。
第 2 步,装配角色。调度器读 spec["roles"],对每个 id 调 load_role,把 frontmatter 里的工具清单挂上、正文当 system prompt。model_analyst 的 confidence_policy: evidence_required 在这一步就生效了,它注定不能凭一句话下结论。
第 3 步,选模型族。进到模型分析节点,route_model_family 再扫一次关键词,「因果推断」命中 <因果推断模型族>,reason="keyword_match" 一并记下。
第 4 步,过门禁。系统把 A 县的数据整理成 dataset_shape,交给 quality_gate 对着 minimum_shape 检查。因果推断对样本要求高,假设这次只凑到 3 个对照县、门槛要 8 个,quality_gate 返回 needs_data,明说「county_count 差 5」。流程在这里停住,不出正式因果结论,只回一份「数据不足、需要补哪些」的说明。这正是第七节那句「不能指望大模型自觉」的地方,把关是代码干的。
第 5 步(假设数据够了),执行与渲染。门禁放行,executor 跑真正的算法,结果写回状态黑板,report 节点按 ARTIFACT_SPECS 的声明渲染成报告,产物落盘。
第 6 步,推导状态。全程每个节点都 add_step 记了痕迹。最后 _derived_status 看黑板:因为这条链 human_review_policy 要求复核,requires_human_review 是 True,于是任务状态被算成 needs_human_review,它不会被误标成 succeeded 直接发出去。
走完这一趟你会发现:大模型只在「角色内部思考、生成文字」时出场,所有「走哪条路、够不够格、算不算成功」的骨架决策,都是确定性代码定的。这就是这套设计的脾气。
九、框架化真正的回报:可测试性
框架化最大的收益,我原以为是「代码整洁」,实际是可测试性。
在我写作时,这个项目的领域层大约 7500 行代码、33 个测试文件。这不是外部基准,而是一个项目内部的可维护性记录;能测得比较密,靠的正是前面那些设计。为什么这些设计就可测?因为第八节里那些骨架决策全是纯函数,给定输入必得同样输出,不依赖大模型、不依赖网络。纯函数是最好测的东西。举个最直白的例子,路由测试长这样:
1 | def test_route_workflow_causal_inference(): |
没有 mock、没有起服务、没有调 API,就是普通的输入到断言。整个领域层的测试大体都是这个形状:
- 注册表可测:
WORKFLOW_CATALOG的结构、每条业务链的必填字段完整性,都是纯数据校验。 - 路由可测:给一个问题,断言它命中哪个 workflow_id 或模型族。因为路由是确定性的,测试不需要 mock LLM。
- 契约可测:
input_specs的校验逻辑、minimum_shape的判定,全是纯函数。 - 门禁可测:给一份缺章节的草稿,断言它必须被拦下来。
- 状态可测:
_derived_status是纯函数,各种组合直接断言。
这就串起来了:因为把决策权从 LLM 手里收回到了确定性代码,所以这些逻辑才可测;因为可测,所以敢改。一个到处是 LLM 自由决策的系统,是没法写出 33 个稳定测试文件的,你连断言什么都不知道,因为同一个输入它每次输出还不一样。
从框架化到声明式,再到确定性、可测试,这条链是通的。
十、几点教训
1. 「万物皆为工具」很优雅,但我的领域里是「万物皆为注册表条目」。 教科书那个统一抽象(把 Memory、RAG、MCP 都变成 Tool)在教学上极有说服力。但我的系统里,需要被统一的不是「能力」而是「声明」:业务链、角色、模型族、产物,各自都是注册表里的一条,工具只是其中一类。统一抽象要选对被统一的那个东西,选错了就会到处别扭。
2. 我一开始过度设计了。 最早的版本里我写了抽象基类、写了插件机制、想做得很「框架」。结果是每加一个功能要动四个文件。后来砍掉大部分抽象,回到「普通函数 + 数据声明」,反而清爽了。抽象的合理数量,取决于你真的有多少个实现,而不是你觉得未来可能有多少个。只有两三个实现的时候就写基类,你是在为一个还没到来的未来付利息。
3. 领域框架有个明确的检验标准。 怎么判断这层框架造对了?看加一个新能力的成本。如果加一条业务链要改路由、改状态、改报告渲染,那这层框架是失败的;如果只是加一条声明加一个节点,那它就在干活。这个标准比「代码看起来优不优雅」靠谱得多,优雅是感觉,改动成本是能数出来的。
4. 别造你能直接用的东西。 这是最省时间的一条。我完整学过怎么手写 LLM 客户端、Message、Agent 基类,学是值得的,因为出问题时我知道底下在发生什么;但生产上再写一遍,纯属浪费。理解和重写,是两件事。
十一、小结
回到开头。「构建你自己的 Agent 框架」这件事,我的答案是:
- 该造的是领域层:业务链注册表、角色注册表、模型族的契约与门禁、产物注册表、节点契约。这些没人能替你造,因为它们编码的是只有你懂的业务约束。
- 不该造的是 LLM 抽象、消息格式、对话循环、工具执行、图编排。这些是标准件,直接用。
而这层领域框架的核心,说白了就一句话:把「加功能」变成「加数据」。能力用声明表达,路由靠查表,合法性靠契约检查,痕迹靠约定留下。这样系统才长得大、测得动,也才敢让别人来改。
教科书教会我的是框架里每一层在干什么,这个理解是我后来敢于「大部分都不造」的底气。看懂了才知道哪层不用自己写,这个顺序不能反:先看懂全貌,才谈得上有取舍地省略。
参考资料
- Hello-Agents 第七章:构建你的智能体框架 - Datawhale
- HelloAgents 框架源码










