“从 0 做 Agent”这件事,我干了快四个月。回头看,真正值钱的不是那几千行代码,而是踩坑之后沉淀下来的 14 个设计决策,以及最后抽象出来的一个通用内核。这个内核让我在四个完全不同的场景里都吃到了“复利”,所以想把这段过程完整摊开讲一讲。
我要先说一个可能得罪人的结论:市面上绝大多数 Agent 框架,都在刻意模糊“框架”和“内核”的边界。你用 LangChain、Dify、CrewAI 跑 demo 很爽,但一旦要上生产、要调优、要换模型、要扛并发,所有被框架藏起来的复杂度都会一次性还给你。我并不是说框架不能用,而是说,如果你连 Agent 底层的循环、状态、工具协议、记忆模型都没有亲手搭过一遍,你很难判断框架里哪一层是你可以信任的,哪一层是正在坑你的。所以这个项目我从一开始就决定:内核自己写,外围能力用成熟库。
1. 先交代背景:为什么我坚持从 0 写 Agent
1.1 最初的认知偏差
我最早对 Agent 的理解非常简单粗暴:Agent = 大模型 + 提示词 + 工具调用。当时我拿一个框架跑通了第一个 demo,那个 agent 能查天气、能搜索、能生成周报,看起来一切都很美好。但随后我试着让它做一个稍微长链路的任务:从一堆邮件里提取任务,按优先级排程,再调用日历工具创建日程,最后给用户发摘要。
结果它要么在某个工具返回后陷入了死循环,要么把上一轮的中间结论丢掉,要么干脆开始编造“已创建日程”但实际上什么都没发生。
我这才意识到,Agent 能不能跑起来,靠的是提示词;但 Agent 能不能稳定地跑完一个长任务,靠的是工程。具体来说是四件事:循环控制、状态管理、工具协议、记忆策略。这四件事,恰恰是框架最想替你藏起来、又隐藏得最差的部分。
1.2 从零开始的底线
决定从 0 做之后,我先给自己划了一条线:不重复造轮子。模型调用底层 SDK 照用,向量库照用,HTTP server 照用,但Agent 的控制流、状态流转、工具注册机制、记忆读写接口必须自己写,而且把规模控制在能一眼看穿的程度。
我给自己定过三个检验问题,如果你也在用框架做 Agent,建议拿来自测:
- 当模型连续调用工具失败十次,循环什么时候停止?
- Agent 的“记忆”在哪个环节写入、哪个环节读取、哪个环节持久化?
- 当用户强制中断任务并修改要求后,Agent 是恢复现场还是重新开始?
如果你回答不出来,那框架对你来说就是黑盒。我不是说黑盒完全不能用,但你要清楚黑盒的代价是什么。我选择从 0 写,是为了让这三个问题都有明确答案,哪怕答案并不完美,但它们是我自己决定的,我知道边界在哪。
2. 14 个设计决策:每一刀都踩过坑
整个开发过程中,我记录了几十个决策点,最后收敛成 14 个真正影响全局的设计决策。我按主题给你拆开讲,每个决策背后都对应着一个实际踩过的坑。
2.1 决策 1-3:哪些事必须自己控制
决策 1:核心循环自己写,不把控制权交给编排框架。
第一版我用的就是框架的 AgentExecutor,导入即用。结果一次升级之后,它对工具调用结果的处理方式悄悄变了,原来会重新组织消息格式,升级后直接把原始结果丢给模型。我的 Agent 行为立刻出现明显退化。
从那时起我把循环控制权收了回来。现在的核心 loop 很朴素,就是“取状态、选策略、调模型、执行工具、写回状态”五个动作,大概两百行代码。关键是,我可以在这五个动作的任何节点插入日志、断点、限流和人工审核钩子。你如果不亲自持有这个循环,后面做安全控制、可观测性、成本限制都无从谈起。
决策 2:模型接入收敛成“一个函数”。
我踩过最蠢的坑,是给各家模型 SDK 各自写了一层封装,结果业务代码里全是 if-else。OpenAI 的 function call、Anthropic 的 tool use、本地模型的 JSON 输出,格式完全不一样。我后来在内核里只对外开放一个统一函数:complete(messages, tools) -> ModelResponse,然后通过 provider adapter 去适配各家协议。
统一之后,换模型只动 adapter,内核和工具层完全无感。这个决策的收益到后期越滚越大,我甚至可以同一套代码在两天内从云端模型切到本地模型。
决策 3:先画状态机,再写代码。
最早我的循环是while true加一堆 if 判断,跑起来完全不可预测。后来我强制自己先画状态机。状态不多,够用就好:IDLE -> PLANNING -> CALLING_TOOLS -> OBSERVING -> DECIDING -> DONE / WAIT_USER。有了状态机之后,任何一个时刻我都能回答“Agent 现在在干什么、能不能继续、下一步需要的条件是什么”。
状态机的存在还让一个东西变成了可能:人工审核位。我可以在调用工具之前插入一个中间态,让 Agent 暂停,等人类确认后再继续。没有状态机的话,“暂停”这个需求会把你逼疯。
2.2 决策 4-7:上下文怎么管才不会崩
决策 4:上下文必须分区,不同生命周期的信息不能混着放。
初版我图省事,把系统提示词、任务描述、工具返回、历史对话全拼成一个 messages 数组塞给模型。跑几个长任务后,上下文窗口被低价值的工具输出占满,模型开始“遗忘”真正的目标。我后来把上下文拆成了四个区:系统区、任务区、工作记忆区、对话历史区。系统区放角色和固定规则,任务区放当前目标,工作记忆区放中间结论和待办,对话历史区才放用户交互。
这个分区的直接好处是,我可以对不同区执行不同的压缩策略。系统区永远不被压缩,工作记忆区是压缩和结构化抽取的重点对象,对话历史区可以做裁剪。混在一起时你根本无法精准处理。
决策 5:每条消息必须带元数据。
我刚开始存消息就是纯文本数组,后来想追溯“这句话到底是用户说的,还是工具输出,还是模型自己生成的工作笔记”,根本无从查起。我现在给每条消息强制带上 sender、type、level、tool_call_id、created_at,其中 type 会区分是 user、assistant、tool_result、system_note。这些元数据是后面做过滤、压缩、评分、可视化的地基。没有这层地基,后面写什么都像在流沙上盖房。
决策 6:显式管理工作记忆,而不是全靠上下文窗口。
“记忆”这个词被做烂了,一说记忆就上向量库。但我踩坑后发现,Agent 在一个任务内最需要的不是长期记忆,而是工作记忆:上一步推导出的结论、还没执行的子任务、已经排除的错误路径。这些东西如果只躺在上下文里,一旦上下文被压缩就全丢了。
我抽象了一个 memory 接口,内部至少三块:短期工作记忆、长期事实记忆、场景偏好记忆。工作记忆不是给模型看全部内容,而是按需把结构化摘要注入上下文。这个过程是显式的,有一个模块在专门管理它,而不是让模型自己在几百条历史里去捞。
决策 7:压缩不是“快满了才做”,而是看内容分散度。
我最早设了个硬阈值,比如上下文超过窗口的 80% 就压缩。后来发现根本不科学。有时候上下文只有 30%,但里面 90% 是同一个工具返回的日志碎片,模型已经被污染了;有时候到了 90%,但因为内容高度结构化,模型反而还撑得住。
后来我改成按内容分散度触发压缩:统计每条来源的占比、重复片段比例、低价值类型消息占比,只要“污染指数”超标就压缩。压缩策略也分三种:摘要压缩、删除低价值日志、结构化事实抽取。这个决策直接让我的 Agent 长任务成功率从不到 30% 提升到了接近 70%。
2.3 决策 8-10:工具接入不能只写一个调用函数
决策 8:统一工具注册协议,工具自己描述自己。
刚开始写工具,就是一个函数一个描述,零零散散塞在代码里。后来工具多了,我发现三个问题:模型不知道该在什么场景下选这个工具、我无法统一做权限控制、工具描述和实现容易脱节。
现在所有工具都要实现一个通用协议:名称、描述、输入 JSON Schema、依赖条件、执行超时、可用角色。注册之后才能被内核发现。这个机制有点像一个餐厅的菜单:后厨可以换菜,但客人(模型)能看到的一定是结构化的菜单,而且菜单上写了辣度、过敏原、预计出餐时间。工具描述的质量和结构化程度,直接影响模型选工具的正确率,这一点怎么强调都不过分。
决策 9:工具返回值必须带状态码和追踪信息。
大多数 Agent 项目里,工具返回就是一个字符串,模型收到后要用语义去猜“这个调用到底成没成功”。这是个巨大的错误设计。工具执行结果应该是一个结构化对象:状态码、数据、日志、耗时。比如一个搜索工具返回了 HTTP 200,但页面内容是 404 页面,这时状态码应该明确告诉你“成功但无有效内容”,而不是让模型去读这堆 HTML 去猜。
我把所有工具返回值统一成ToolResult(status, data, meta)。status 区分 success、failure、partial、empty。meta 里带执行时间、token 消耗、原始日志。这一步让 Agent 的纠错能力提升了一个档次,因为它能精确地区分“工具挂了”和“工具没找到东西”,从而决定是重试、换工具,还是直接告诉用户无结果。
决策 10:工具执行必须有超时、重试和幂等设计。
一个会真实挂掉的工具,比一个从不报错的工具安全得多。我一开始工具超时设的 60 秒,Agent 卡住半个小时没有反馈。后来我给每个工具配了默认超时、最大重试次数和幂等策略。对于创建类工具,比如发邮件、建日程,必须带 request_id 实现幂等,否则一次重试就会产生两封邮件、两个日程。
这个决策还牵出一个更微妙的坑:模型发起工具调用,工具执行超时了,但服务端其实已经处理成功。这时候 Agent 如果盲目重试,就是重复操作;如果不重试,就可能丢结果。幂等设计就是解决这个问题的:重试时带上 request_id,服务端会自动识别并返回上一次结果。
2.4 决策 11-14:可靠性、可观测性与评估
决策 11:安全边界必须内建于内核,而不是由外部流程保证。
Agent 的能力越大,破坏力越大。我给内核内置了几个硬性护栏:最大循环次数、单轮任务最大工具调用数、工具级权限矩阵、人工审核位、成本上限。这些不是外围的监控,而是内核循环里强制检查的约束。比如最大循环次数用完了,Agent 必须进入 DONE 状态并向用户输出当前进展,而不是继续硬撑。
这个决策的重要性,在 Agent 被做成服务对外提供时彻底体现出来。没有这些内置护栏,一个失控循环就能把你的模型账单打爆,或者在生产环境里反复调用删除接口。安全不是事后加的补丁,它应该是状态机的一部分。
决策 12:可观测性默认全量开,不做开关。
很多 Agent 框架讲“可观测性”都是事后插桩,我一开始也这样,结果出问题时找不到是哪一轮推理导致的行为异常。后来我改成默认全量记录:每轮循环都记录完整的状态快照、模型输入输出、工具返回、token 消耗、耗时。数据量确实大,但这个成本不能省。
我给自己写了一个简单的 trace viewer,可以按会话回放 Agent 的每一步。排查问题时,我几乎不再靠猜,而是直接看“模型在这一步看到的上下文到底是什么”。如果你也在做 Agent,我建议你把 trace 当一等公民,而不是 debug 时才想起的东西。
决策 13:评估集第一天就建,不要等“功能做完了再补”。
这是我所有决策里最后悔没早做的。我前两个月基本靠手工测试,改一个 Prompt 就要手跑十几个场景,效率极低,而且改坏了自己都不知道。后来我建了一个评测集,里面放三类样本:正常任务、边界任务、对抗任务。每次改动后跑一遍回归,看成功率、耗时、token 消耗的变化。
这个评测集到后期变成了我最重要的资产之一。每次模型升级、提示词调整、架构重构,我都会跑一遍。它可以让我勇敢地做大胆改动,因为它能兜底。没有这个评测集,我根本不敢碰已经能跑的代码。
决策 14:多 Agent 之间不共享“对话”,只共享任务队列和记忆存储。
我后期做了多 Agent 编排,一开始天真地想让两个 Agent 直接对话,用一个共享消息总线。结果那个调试体验堪称灾难:两个 Agent 互相等对方、环消息满天飞、上下文越滚越脏、责任永远分不清。后来我改了架构:多 Agent 之间不直接对话,而是通过任务队列加记忆存储协作。每个 Agent 只要管好自己的输入空间和输出空间,像一个流水线工人,从队列拿工单,做完放回结果区。
这个决策同时解决了并发问题。多个会话可以并行跑,因为全局状态是隔离的;Agent 实例是近乎无状态的,session 状态按 session_id 隔离存储。归根到底一句话:Agent 的并发不是线程问题,是状态隔离问题。
2.5 14 个决策速查表
我把 14 个决策整理成一张速查表,方便你对照自己的项目检查。
| 决策编号 | 决策方向 | 典型坑 | 最终方案 |
|---|---|---|---|
| 1 | 循环控制 | 框架升级导致行为漂移 | 核心循环手写,约 200 行 |
| 2 | 模型接入 | 各家 SDK 协议不统一 | 统一 complete 函数 + adapter |
| 3 | 状态管理 | while true 不可控 | 显式状态机 + 人工审核位 |
| 4 | 上下文分区 | 工具日志打爆上下文 | 系统/任务/工作记忆/历史四分 |
| 5 | 消息结构 | 无法追溯信息来源 | 消息强制携带元数据 |
| 6 | 记忆策略 | 长任务中模型遗忘坐标 | 显示工作记忆接口 + 三种记忆 |
| 7 | 上下文压缩 | 只看窗口占比不科学 | 按内容分散度触发压缩 |
| 8 | 工具协议 | 模型不会正确选工具 | 统一注册协议 + 结构化描述 |
| 9 | 工具返回 | 无法区分“挂了”和“没结果” | 结构化的 ToolResult + status |
| 10 | 工具健壮性 | 重试导致重复操作 | 超时 + 重试 + 幂等 request_id |
| 11 | 安全边界 | 失控循环打爆账单 | 内置轮数/权限/成本/人审护栏 |
| 12 | 可观测性 | 出错时看不到模型上下文 | 默认全量 trace + 会话回放 |
| 13 | 评估体系 | 改动没有兜底 | 第一天就建评测集并持续回归 |
| 14 | 多 Agent 协作 | 共享消息总线导致死锁 | 任务队列 + 记忆存储,不直接对话 |
3. 通用内核:到底沉淀了什么
3.1 内核的四块拼图
做完这 14 个决策之后,我意识到自己实际上已经完成了一个通用内核的雏形。它不是框架,不提供开箱即用的业务功能,它只解决一个核心问题:给 Agent 提供稳定的运行时环境。这个内核由四块拼图组成。
第一块是Loop,也就是状态机驱动的循环。它决定了 Agent 从开始到结束的生命周期,是内核的最小骨架。第二块是ToolRegistry,所有工具通过统一协议注册进来,内核不关心工具怎么实现,只关心它能不能被模型正确发现和调用。第三块是Memory,它不是一个数据库,而是一组读写接口,工作记忆、长期记忆、偏好记忆都通过这组接口读写。第四块是Policy,安全护栏、成本限制、人工审核规则、评估 hooks,都属于 Policy 层。
这四块拼图里面,Loop 是主动脉,其余三块通过接口注入。我刻意把“正在执行的 Agent”和“为 Agent 提供的环境”分开了。那个提供环境的东西,我其实叫它 harness 更准确:它不扮演任何角色、不做任何决策,只负责让 Agent 的决策过程更安全、更可控、更可观测。很多项目失败,就是因为把 harness 当成了 Agent,让环境替 Agent 做决策;或者反过来,让 Agent 去处理环境级别的安全问题,两头都拧巴。
3.2 一个最小可用内核的骨架
我贴一段极简伪代码,去掉了很多适配细节,但保留了内核的主干。这段代码帮我回答过无数次“这个 Agent 到底是怎么转起来的”:
class AgentKernel: def __init__(self, model, registry, memory, policies): self.model = model # 统一模型接口 self.registry = registry # 工具注册表 self.memory = memory # 记忆接口 self.policies = policies # 安全/成本/评估钩子 def run(self, session_id, task): state = State.new(session_id, task) self.memory.init(session_id) while not state.is_terminal(): self.policies.check(state) # 循环上限、成本等护栏 step = state.next_step() if step == "planning": state.plan = self.model.plan(task, self.memory.load(session_id)) elif step == "calling_tools": if not state.pending_tools: state.pending_tools = self.model.decide_tools( state.plan, self.registry.describe_all() ) results = [] for call in state.pending_tools: result = self.registry.execute(call) # 统一 ToolResult results.append(result) self.memory.record_tool_results(session_id, results) state.pending_tools = [] elif step == "deciding": new_plan = self.model.act( state.plan, self.memory.load(session_id) ) if new_plan.is_final(): state.response = new_plan.response state.finish() else: state.plan = new_plan self.policies.observe(state) # 全量 trace return state.response这段伪代码总共不到四十行,但它就是整个 Agent 的骨架。状态机提供了“可中断、可恢复、可审核”的结构,工具注册表让模型只看到它能用的东西,记忆接口让上下文管理变得显式,Policy 层把安全放进了循环本身。
这个内核最重要的特性是:业务逻辑完全外置。Agent 是一个什么样的角色、它需要哪些工具、它如何表达记忆偏好,全部通过注入的方式配置。内核不关心你写的是客服 Agent、代码助手还是数据分析 Agent,它只保证任何 Agent 都能跑在一个“有边界、可观察、可恢复”的运行时里。
4. 四种复利:为什么这套内核越用越值钱
做通用内核最大的回报,不是一次交付,而是后续每次复用都在“吃老本”。我把这段经历总结成四种复利:技能复利、记忆复利、评估复利和架构复利。
4.1 技能复利:写一次 skill,处处可装
第一次吃到的红利,来自“技能包”机制。内核的工具注册协议稳定后,我发现可以把工具按场景打包成 skill:一个“把网页保存成 Markdown”的 skill、一个“公司知识库检索”的 skill、一个“从邮件提取结构化任务”的 skill。每个 skill 包含工具描述、依赖检查、初始化逻辑和一段 skill 说明。
这个机制的复利在于,同一个 skill 可以被完全不同的 Agent 安装使用,无需修改内核。我的 CLI 助手装了“网页转 Markdown”技能,我的周报 Agent 也装了同样的技能,底层是同一份代码。新场景开发变成了“选技能包 + 写角色提示词”的组合操作,越往后技能库越厚,新 Agent 的搭建速度越快。想做好 skill,关键是按“场景单元”而不是“函数单元”来打包:一次解决用户一个完整诉求,而不是暴露一堆散装函数。
4.2 记忆复利:积累的记忆跨 Agent 复用
第二份复利来自记忆模块的接口统一。内核只认一组读写接口,底层实现可以是向量库、SQLite、JSON 文件或者 Redis。接口统一之后,一个 Agent 积累的数据可以被另一个 Agent 无缝读取。
这给我带来了一个很实在的场景:导购 Agent 积累了用户的品牌偏好,客服 Agent 在处理用户售后问题时,通过同一个记忆接口读到了这份偏好记录,于是沟通方式自动调整。这个能力不是靠“把所有数据灌进一个大模型”实现的,而是靠记忆数据的结构化沉淀和接口的标准化。记忆复利的前提是你要有清晰的写入策略:哪些信息值得长期存储、什么时候写入、以什么结构存储。如果只是把对话记录直接扔进向量库,那叫日志,不叫记忆。
4.3 评估复利:每踩一个坑,补一个用例
第三份复利是评测集的自我增值。我从第一天开始建评测集,每踩一个 bug、每发现一个边界情况,就往里面补一条用例。刚开始只有十条,半年后积累了八百多条。
复利体现在多个维度。第一,回归测试保住了旧功能,我敢放心重构内核。第二,评测集变成了“能力地图”,每一条用例背后都对应着一个曾经修过的坑,你只要看评测集就能知道这个 Agent 的能力边界和历史教训。第三,评测集让我换模型时有据可依。实测下来,每次换模型,我先跑一遍评测集,成功率一目了然,比我手动点几十个页面高效得多。
评估集和 bug 记录是同一个动作:你踩过的坑如果不变成评测用例,将来一定还会再踩。这个习惯的价值,怎么强调都不过分。
4.4 架构复利:同一个内核长成四种产品形态
第四份复利,是我觉得最有意思的:同一个内核最后变成了四种完全不同的产品形态,而四者共享了同一套循环、安全、记忆和工具协议。
第一个形态是命令行 Agent。内核直接嵌进 Python 脚本,一个人机循环跑在终端里,适合快速验证技能包、调试工具协议、做日常自动化。第二个形态是HTTP 服务形态。内核跑在 FastAPI 后端,每次请求创建一个 session,session 状态隔离在后端存储里,这就是能扛并发的版本。这里的关键是内核实例本身无状态,状态全部挂在 session_id 下,并发度不再是瓶颈。
第三个形态是多 Agent 编排形态。我在内核外面包了一层调度器,多个内核实例通过任务队列协作,而不是直接对话。底层复用的还是同一个内核,只是把输入输出管道接到了队列上。第四个形态是嵌入式本地工具。我把内核核心裁剪成一个小包,放进本地笔记工具里,做一个离线笔记整理助手。它不依赖云端模型,对延迟和成本极度敏感,但因为内核组件是可替换的,我只需要换掉模型 adapter 和记忆存储实现就能跑起来。
一个内核四种形态,收益不是四倍,而是很多倍。因为四个形态的 bug 修复、安全策略、技能包全部是共享的。我修好一个循环控制的边界问题,四个产品形态同时受益。
5. 常见问题与排查技巧实录
5.1 一个个排查过的硬问题
我在开发中遇到过不少看似诡异的问题,挑几个有代表性的说说。
问题一:模型死活不调用工具,一直在向用户要信息。最开始我以为是提示词不够强硬,反复写“你必须使用工具”,效果甚微。后来看 trace 才发现,模型根本看不到任何工具的调用示例。我在系统区加了一段“工具使用示范”,展示用户意图、工具选择、参数构造的完整映射,行为立刻改善。这给我的经验是:模型不调工具,多半不是它不愿意,而是它不知道什么时候该调。
问题二:Agent 在长任务里反复做同一件事。比如已经搜索过某个关键词,结果下一轮又搜了一遍。这个问题的根因是工作记忆没有记录“已完成的操作”。我在工作记忆里加了一个“已尝试路径”列表,每次决策前把已做过的操作和对应结果给模型看,重复率立刻下降。
问题三:并发上去之后状态串了。最初我在内核里用了一个全局变量存当前状态,两个会话一起跑就互相覆盖。后来所有状态一律挂在 session_id 下,每个 session 独立的 memory 空间,全局变量清零。并发这事,说到底是状态隔离的工程问题,不是线程数的问题。
问题四:工具明明成功了,Agent 却告诉用户失败了。这类问题的根源通常不是工具,而是模型看不到工具返回的完整结构。当时我的工具返回只有一段文本,模型误读。改成结构化 ToolResult 之后,状态一目了然,误判率大幅下降。
5.2 排查工具与手段
排查问题的核心手段就一条:回到 trace 里看模型实际看到的上下文。我几乎极少靠猜解决 Agent 问题。每次出问题,打开 trace viewer,回放到出错前的那一轮,看状态快照、模型输入、工具返回、成本消耗。90% 的问题在看完 trace 的瞬间就有了答案。
我还养成了一个习惯:给每个 Agent 写一份“诊断提示词”。当 Agent 自己觉得困惑时,它可以调用一个工具把自己的当前状态和计划原文发给调试者。这个工具像一条紧急求助通道,效果出奇的好,因为它暴露的是 Agent 的第一人称视角,而不是你从外部推测的。
6. 写在最后:关于“从 0 做”这件事的个人体会
如果我只能分享一条个人经验,那就是:Agent 的核心工作不是写提示词,而是定义循环。提示词决定了一次回答的质量,循环决定了整个系统的上限。一个不稳定的循环,配再强的模型也会在长任务里翻车;一个稳定可控的循环,配中等模型也能在特定场景里做出可靠的结果。
我第一次跑通多 Agent 协作时,两个 Agent 之间互换的不过是一条 JSON 格式的工单和一段结论文本,系统提示词只有两句。那一刻我意识到,Agent 工程的重点在于构造可靠的运行时和清晰的协作协议,而不是堆砌花哨的提示词技巧。
如果你正打算从 0 做 Agent,我的建议是:别急着去评测哪个框架更好用,先花一天时间,手动写一个两百行的循环,体验一下“自己控制每一轮决策”的感觉。等你对状态、工具、记忆这三件套有一手感觉之后,再回来看框架,你会瞬间明白它的设计取舍。这个基本功,绕不过去,但它绝对值得你花这个时间。