☰
从零手搓Agent内核:14个设计决策与四种复利效应
2026/10/8 10:54:39 网站建设 项目流程

1. 为什么我要从零手搓一个 Agent 内核

去年年底我接手了一个内部工具链项目,需求说起来很简单:让一个 AI 助手能自动完成"读需求文档、查代码库、改配置、跑测试、写变更记录"这一整条链路。当时团队里有人提议直接用现成的编排框架,我试了一圈,发现一个很尴尬的问题——大部分框架把"能跑通 Demo"和"能长期维护"之间的鸿沟藏得太深了。Demo 阶段你写十几个节点就能跑,一旦要加权限控制、要接内部审计、要做失败重试和状态回滚,整个图就变成了一团意大利面。

所以我决定从零写一个 Agent 内核。这个决定让我在接下来三个月里踩了 14 个设计上的坑,也最终沉淀出一个通用内核,并且意外地发现它在四个完全不同的场景里都能复用,产生了复利效应。这篇文章就是把这 14 个决策和四种复利讲清楚,适合正在做 Agent 开发、纠结要不要自研内核、或者已经用框架但被框架反噬的同学。我会尽量说人话,把每个决策背后的"为什么"讲透,而不是甩一堆架构图让你自己悟。

先说结论性的判断:Agent 开发真正的难点从来不是"调用大模型",而是状态管理、工具边界、失败恢复这三件事。框架帮你解决的是第一层"能跑",但第二层"跑得稳"和第三层"跑得省"必须靠你自己想清楚。下面我按踩坑的时间顺序,把 14 个决策拆开讲。

2. 十四个设计决策的逐条拆解

2.1 决策一:内核与业务必须彻底解耦

我最初的版本把"读文档"这个业务逻辑直接写进了主循环里,结果第二个场景要用的时候,发现主循环里全是第一个场景的假设。痛定思痛,我把内核抽象成三个纯粹的概念:消息(Message)、工具(Tool)、状态(State)。内核只负责"给定状态和消息,决定下一步调用哪个工具",至于工具内部干什么,内核一概不管。

这个决策的价值在后面才显现出来。当我要接入第二个场景时,只需要注册新的工具集,内核代码一行没改。判断标准很简单:如果你删掉所有业务工具,内核还能编译通过并且跑一个空循环,那解耦就是成功的。

2.2 决策二:状态用不可变数据结构

一开始我用可变字典存状态,调试时经常出现"这个字段什么时候被改的"这种灵魂拷问。后来改成每次状态变更都返回一个新对象,配合一个变更日志。代价是内存占用上去了,但换来的是可回放、可对比、可快照。Agent 出问题时,我能把任意一步的状态 dump 出来,重放那一步,这在排查"为什么它突然调了个莫名其妙的工具"时简直是救命稻草。

2.3 决策三:工具描述要当成 API 文档来写

我踩过最蠢的坑是工具描述写得太随意。比如一个search工具,我写的是"搜索相关内容",结果模型经常传一些它自己编的参数名。后来我把每个工具的描述当成给新人的 API 文档来写:参数类型、取值范围、返回结构、什么情况下不该用,全部写清楚。改完之后工具调用成功率肉眼可见地上升。这里有个经验:描述里明确写"当 X 情况时不要调用本工具",比写十句"本工具很好用"都管用。

2.4 决策四:给工具加"预算"而不是"超时"

超时是时间维度的限制,但 Agent 真正失控往往是调用次数失控。我见过一个 Agent 在 30 秒内调了 200 次搜索工具,每次都超时没触发,但整体把配额烧光了。所以我在内核里给每个工具加了调用次数预算和 token 预算,超预算直接熔断并返回一个明确的错误,让模型知道"你这条路走不通了,换一条"。

2.5 决策五:错误要分类,不能一锅端

最初我把所有异常都当成"工具失败"返回给模型,结果模型分不清"参数错了"和"服务暂时不可用",重试策略完全乱套。后来我把错误分成四类:参数错误(不可重试)、临时故障(可重试)、权限不足(需升级)、业务拒绝(需换方案)。每类错误返回给模型的措辞都不一样,模型的处理方式也就对了。

2.6 决策六:记忆分层,别把什么都塞进上下文

我一开始图省事,把所有历史消息都塞进上下文,结果 token 爆炸,而且模型被无关信息干扰。后来分成三层:工作记忆(当前任务相关)、会话记忆(本次对话摘要)、长期记忆(跨会话的事实)。工作记忆全量保留,会话记忆定期压缩成摘要,长期记忆只在需要时检索。这个分层让上下文长度稳定在一个可控范围。

2.7 决策七:插件化不是目的,是手段

热词里"插件化"很火,但我踩的坑是为了插件化而插件化。早期我设计了一套复杂的插件注册机制,结果每个插件都要写一堆样板代码。后来我简化成"一个工具就是一个函数加一份描述",注册就是往列表里 append。插件化的真正价值是让新增能力不需要改内核,而不是搞一套花哨的加载器。

2.8 决策八:编排逻辑要能"看得见"

Agent 最让人不信任的地方是"黑盒"。我做了一个决策轨迹记录器,把每一步的"输入状态、候选工具、最终选择、理由"都记下来。这个记录器后来成了团队 review Agent 行为的主要依据。你不需要可视化界面,一个结构化的 JSON 日志就够了,关键是每一步都要能解释。

2.9 决策九:并发要谨慎,默认串行

我一度想让 Agent 并行调用多个工具来提速,结果引入了状态竞争和顺序依赖的 bug。后来改成默认串行,只有明确无依赖的只读工具才允许并行。这个决策牺牲了一点速度,但换来了可预测性。Agent 的可预测性比速度重要得多。

2.10 决策十:给模型"退出"的明确信号

早期我的循环没有明确的终止条件,模型有时候会一直"再想想"。后来我定义了一个finish工具,模型必须显式调用它并给出最终答案,循环才结束。同时设了最大步数兜底。这个决策让 Agent 的行为边界清晰了很多。

2.11 决策十一:提示词要版本化

提示词改动对 Agent 行为的影响巨大,但我一开始是直接在代码里改字符串,改完就忘了之前是什么样。后来我把提示词抽成独立文件并加版本号,每次改动都记录"改了什么、为什么改、效果如何"。这个习惯让我在行为回退时能快速定位是哪次提示词改动导致的。

2.12 决策十二:评测集要早建,哪怕很粗糙

我拖到项目中期才建评测集,导致前面很多改动都是"感觉变好了"。后来我建了一个 50 条的小评测集,覆盖典型任务和边界情况,每次改动跑一遍。评测集不需要多完美,能挡住明显回退就够了。这是投入产出比最高的一个决策。

2.13 决策十三:沙箱不是可选项

Agent 会执行代码、改文件、发请求,这些操作必须有边界。我给所有有副作用的工具套了一层沙箱:文件操作限制在指定目录,网络请求走白名单,代码执行有资源限制。这个决策在后期救了我一次——一个模型生成的脚本试图遍历整个文件系统,被沙箱挡住了。

2.14 决策十四:内核要能被"单测"

最后一个决策是把内核逻辑和模型调用解耦,用一个假的模型(返回预设的工具调用序列)来单测内核。这样我能在不花钱、不联网的情况下测试循环、状态管理、错误处理。这个决策让内核的稳定性上了一个台阶。

3. 通用内核的四种复利

3.1 复利一:跨场景复用,边际成本趋近于零

当内核稳定后,我把它用到了四个场景:代码助手、文档问答、数据清洗、运维巡检。每接一个新场景,工作量主要集中在"写工具"和"调提示词",内核几乎不动。这就是复利——第一次投入大,后面每次复用成本极低。我算过一笔账,第二个场景的开发时间只有第一个的三分之一。

3.2 复利二:能力沉淀,工具库越用越厚

因为工具是插件化的,我在做第二个场景时写的工具,第三个场景直接拿来用。比如"读文件"、"搜索"、"执行命令"这些基础工具,四个场景共享。工具库像滚雪球一样变大,新场景启动时能直接站在前面积累的肩膀上。

3.3 复利三:问题排查经验可迁移

在内核上踩过的坑,比如错误分类、预算控制、状态回放,在四个场景里都是通用的。我排查第一个场景的经验,直接用在后面三个上。这种经验的可迁移性是自研内核相对用框架最大的隐性收益——你真正理解了系统,而不是被框架的黑盒牵着走。

3.4 复利四:评测与观测体系复用

评测集和决策轨迹记录器也是内核级的,四个场景共享同一套观测体系。这意味着我可以用统一的指标对比不同场景的表现,也能把某个场景发现的问题快速验证是否在其他场景也存在。观测体系的复用让质量把控变得系统化,而不是每个场景各搞一套。

4. 实操中的关键环节与配置

4.1 内核主循环的伪代码结构

下面是我内核主循环的简化结构,用 Python 风格伪代码表示,重点是逻辑而非语法:

def run_agent(state, tools, max_steps=20): for step in range(max_steps): # 1. 记录当前状态快照 trace.record(state) # 2. 让模型基于状态决定下一步 decision = model.decide(state, tools.descriptions()) # 3. 检查预算 if budget.exceeded(decision.tool_name): state = state.with_error("预算超限,请换方案") continue # 4. 执行工具(带沙箱) if decision.tool_name == "finish": return decision.answer result = sandbox.execute(tools[decision.tool_name], decision.args) # 5. 更新状态(不可变) state = state.append(decision, result) return "达到最大步数,未完成"

这个结构看起来简单,但每一行背后都是前面 14 个决策的体现。比如trace.record对应决策八,budget.exceeded对应决策四,sandbox.execute对应决策十三。

4.2 工具描述的模板

我用的工具描述模板大致如下,这个模板是踩了决策三的坑之后定下来的:

{ "name": "search_code", "description": "在代码库中搜索匹配的代码片段。当需要定位某个函数或变量的定义时使用。当只是想知道文件是否存在时,不要用本工具,改用 list_files。", "parameters": { "query": {"type": "string", "description": "搜索关键词,支持正则"}, "path": {"type": "string", "description": "搜索范围,默认为仓库根目录"} }, "returns": "匹配的代码片段列表,每项包含文件路径、行号、内容" }

注意description里明确写了"什么时候不要用",这是提升调用准确率的关键。

4.3 错误分类的返回措辞

不同错误类型返回给模型的措辞我做了区分,实测下来模型的处理方式明显更合理:

错误类型返回措辞示例模型预期行为
参数错误参数 query 缺失,请补充后重试修正参数重试
临时故障服务暂时不可用,可稍后重试换工具或等待
权限不足当前无权限访问该路径换路径或报告
业务拒绝该操作被策略拒绝,请换方案放弃该路径

4.4 状态快照的存储格式

状态快照我用 JSON Lines 存储,每行一个步骤,方便追加和回放:

{"step": 1, "state_hash": "a1b2", "tool": "read_file", "args": {"path": "req.md"}, "result_summary": "读取成功,1200字"} {"step": 2, "state_hash": "c3d4", "tool": "search_code", "args": {"query": "config"}, "result_summary": "命中3处"}

state_hash让我能快速判断两步之间状态是否真的变了,排查"空转"问题特别有用。

5. 常见问题与排查技巧实录

5.1 模型反复调用同一个工具怎么办

这是最常见的失控模式。我的排查顺序是:先看工具描述是不是有歧义,再看错误返回是不是让模型误以为"再试一次就好",最后看预算是不是没设。大部分情况下,把错误措辞从"失败"改成"该路径不可行,请换方案"就能解决。

5.2 上下文越来越长导致变慢变贵

先检查记忆分层有没有做。如果做了还长,看是不是工作记忆里塞了太多工具返回的原始数据。我的做法是工具返回时只保留摘要,原始数据存到外部,需要时再检索。这个改动让我的平均上下文长度降了 60%。

5.3 换了模型后行为大变

这是提示词版本化(决策十一)发挥作用的地方。我会用评测集跑一遍新模型,对比通过率和步数。如果差异大,先看是不是新模型对工具描述的敏感度不同,通常微调描述就能拉回来。

5.4 排查速查表

现象可能原因排查动作
空转不结束缺 finish 信号或预算检查终止条件和预算
工具调用参数错描述不清补全参数说明和反例
状态错乱可变状态被共享改不可变数据结构
偶发失败并发竞争改回串行
行为回退提示词改动对比版本记录

5.5 几个我踩过的独家坑

第一个坑是工具返回了非结构化文本,模型解析起来很吃力。后来我强制所有工具返回结构化数据,模型的理解准确率明显提升。第二个坑是在提示词里写了太多示例,导致模型过度模仿示例而不会举一反三。示例控制在 2 到 3 个就够了。第三个坑是忘了给只读工具和写工具做区分,导致模型在只读阶段就尝试改文件。给工具打上readonly标记后,我能在编排层做更细的控制。

6. 我对这套内核的后续扩展想法

这套内核目前跑得挺稳,但我还在持续打磨。接下来想做的方向有几个:一是把决策轨迹做成可查询的,方便按"工具调用序列"检索历史案例;二是把评测集从 50 条扩到 200 条,覆盖更多边界;三是探索多 Agent 协作,但前提是单 Agent 内核足够稳,否则多 Agent 只会把问题放大。

我个人在实际操作中的体会是,Agent 开发最忌讳的就是"先跑起来再说"。跑起来很容易,但跑起来之后你会发现所有没想清楚的设计决策都会以 bug 的形式回来找你。那 14 个决策里,有至少一半是我在返工中补上的。如果你正准备从零做 Agent,我的建议是先把状态管理、工具边界、失败恢复这三件事想清楚,再动手写第一行代码。这比任何框架都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询