ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从0构建Agent:14个设计决策与通用内核实践

从0构建Agent:14个设计决策与通用内核实践 “从 0 做 Agent”这件事我干了快四个月。回头看真正值钱的不是那几千行代码而是踩坑之后沉淀下来的 14 个设计决策以及最后抽象出来的一个通用内核。这个内核让我在四个完全不同的场景里都吃到了“复利”所以想把这段过程完整摊开讲一讲。我要先说一个可能得罪人的结论市面上绝大多数 Agent 框架都在刻意模糊“框架”和“内核”的边界。你用 LangChain、Dify、CrewAI 跑 demo 很爽但一旦要上生产、要调优、要换模型、要扛并发所有被框架藏起来的复杂度都会一次性还给你。我并不是说框架不能用而是说如果你连 Agent 底层的循环、状态、工具协议、记忆模型都没有亲手搭过一遍你很难判断框架里哪一层是你可以信任的哪一层是正在坑你的。所以这个项目我从一开始就决定内核自己写外围能力用成熟库。1. 先交代背景为什么我坚持从 0 写 Agent1.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 函数 adapter3状态管理while true 不可控显式状态机 人工审核位4上下文分区工具日志打爆上下文系统/任务/工作记忆/历史四分5消息结构无法追溯信息来源消息强制携带元数据6记忆策略长任务中模型遗忘坐标显示工作记忆接口 三种记忆7上下文压缩只看窗口占比不科学按内容分散度触发压缩8工具协议模型不会正确选工具统一注册协议 结构化描述9工具返回无法区分“挂了”和“没结果”结构化的 ToolResult status10工具健壮性重试导致重复操作超时 重试 幂等 request_id11安全边界失控循环打爆账单内置轮数/权限/成本/人审护栏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 后端每次请求创建一个 sessionsession 状态隔离在后端存储里这就是能扛并发的版本。这里的关键是内核实例本身无状态状态全部挂在 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我的建议是别急着去评测哪个框架更好用先花一天时间手动写一个两百行的循环体验一下“自己控制每一轮决策”的感觉。等你对状态、工具、记忆这三件套有一手感觉之后再回来看框架你会瞬间明白它的设计取舍。这个基本功绕不过去但它绝对值得你花这个时间。
返回列表