如何设计一个 Agent 系统:5 个决策,一套能跑的实现

Claude Code、Cursor、Codex 这一年把“Agent”从概念变成了日常工具。但当一个团队开始认真考虑“我们自己搭一个 Agent 系统”——不管是 Coding Agent、客服 Agent 还是数据分析 Agent——很快会发现市面上的资料两极分化:不是“接个 API 调个循环”的玩具教程,就是某个具体框架的使用文档。

当中缺失的那块拼图,是设计决策层:一个生产级的 Agent 系统到底由什么组成,每一层有哪些实现路径,各自的权衡是什么,做出选择后又该如何落地。

这篇文章补的就是中间这块:五个设计决策,每个决策先讲通用原则,再给落到代码和参数的实现。贯穿全文的实例是 Coding Agent——它是所有 Agent 类型里工具链最重、反馈最直接、风险也最典型的一种,把它想清楚之后,再理解其他领域的 Agent,许多设计问题都会变得更清晰。

本文实现部分来自“牛码”。牛码不是七牛云的产品,是小编为本次实践搭建的个人项目。借助“牛码”的案例来验证这五个决策到底是能落地的工程判断,还是纸上谈兵。模型 API、存储和 CDN 用的全是七牛云对外开放的能力,也就是说:这套东西,你在业余时间也能搭起来

为什么越来越多团队选择自建

使用现成工具当然更便捷,但企业级用户很快会遇到三个跨领域的现实问题:

成本结构****不可控。通用工具按 token 计费且模型不可选,团队规模一大,账单里有大量“用大炮打蚊子”的浪费——简单任务和复杂任务打到同一个最贵的模型上。

数据出不去。代码、客户对话、经营数据要发到第三方(往往是海外)的服务器,涉及保密的团队天然会犹豫。

闭环断在最后一步。通用工具只做到“生成结果”,任务就结束了:代码写完了要你部署,回复草稿拟好了后要你发送,报表生成了要你来归档。Agent 和你的基础设施是两套体系。

而这三个问题的共性不是工具不好,而是它不为你的场景定制。自建 Agent 的价值就在这三点——成本可路由、数据留在自己的基础设施里、闭环接到自己的系统上。

一个 Agent 系统的四层架构

抛开具体产品,任何 Agent 系统本质上是四层东西叠起来的:

  • 大脑(模型层):负责理解任务、生成内容、做判断;

  • 循环(编排层):让模型不再“问一句答一句”,而是能规划、调用工具、看结果、再决定下一步;

  • 手脚(工具层):是操作外部世界的能力——Coding Agent 是读写文件、执行代码,客服 Agent 是查订单、开工单,现在一般以 MCP 工具的形式接入;

  • 闭环(落地层):决定结果生成之后怎么真正生效,这一层是绝大多数通用工具的空白。

牛码的架构就是这四层的直译:

1

四层各对应一个核心设计决策,加上贯穿全部四层的安全设计,一共五个决策。

决策一:模型——单一供应商还是多模型路由

原则上不同任务对模型能力的要求差别极大,把所有请求打到同一个模型上,不是在简单任务上浪费钱,就是在复杂任务上牺牲质量。生产级系统应该按任务特征路由到不同模型,并且接入层要协议兼容,保证供应商可随时更换——模型市场无时无刻不在洗牌,锁死在单一供应商上的系统会跟着供应商一起过时。

牛码的模型层走七牛云 AI 大模型推理服务,一个 API 同时兼容 Anthropic 和 OpenAI 协议,认证用标准的 ANTHROPIC_AUTH_TOKEN + ANTHROPIC_BASE_URL 配置,密钥集中托管、不下发到开发者本地是生产化目标(MVP 阶段 Key 仍存在本地配置文件里)。换模型供应商不用改一行接入代码——这是“不锁死”原则的基础设施前提。

多模型路由的实现有三条路:让模型给任务复杂度打分再路由、写死规则路由、完全交给用户手动选。这边权衡下来,结论是规则先行、手动覆盖兜底,自动打分出局

不采用打分机制的理由是,如果让模型先给任务打分再决定用哪个模型,本身多了一次模型调用,延迟和成本都会涨,而且打分结果不稳定——同一个任务两次打分可能落在不同档位,调试时完全无法复现。规则路由虽然“笨”,但是可预测、可调试、零开销。

具体的路由规则有三条,系统按顺序判断,命中后即停止:

  1. 用户指定优先。命令带 --model thinking 或会话里说“用推理模型”,直接切档。这是逃生舱——任何自动规则都可能判断错,必须给人留手动覆盖的口子。

  2. 任务特征触发推理档。任务描述命中“架构 / 重构 / 设计方案 / 性能优化 / 排查”等关键词,且涉及文件数 > 5 或首轮计划步骤数 > 8,就路由到 z-ai/glm-5.2。注意,这里是“且”不是“或”——只有关键词没有规模,日常档足够。

  3. 其余全部走日常档 deepseek/deepseek-v4-flash

具体实现可以参考编排层入口处的代码:

REASONING_KEYWORDS = ["架构", "重构", "设计方案", "性能优化", "排查"]

def route_model(task: str, file_count: int, plan_steps: int, override: str | None) -> str:
    if override:                                    # 规则 1:显式指定
        return MODEL_MAP[override]
    hit = any(k in task for k in REASONING_KEYWORDS)
    if hit and (file_count > 5 or plan_steps > 8):  # 规则 2:关键词 + 规模
        return "z-ai/glm-5.2"        
    return "deepseek/deepseek-v4-flash"             # 规则 3:默认日常档

这里有一个细节,首轮计划生成之前拿不到 plan_steps 的值,所以实际顺序应该是——首轮统一用日常档生成计划,计划出来后步骤数超阈值,从第二轮起升档。这就避开了“为了路由先问一次模型”的鸡生蛋问题。

其实,设计方案原本还有第三档(简单快速任务用小模型),碍于实现时间的问题,最后遗憾砍掉了。补全、格式化这类任务在 Agent 循环里占比极低,主要是被 IDE 处理了,单独维护一档路由的复杂度和收益不成正比。三档简化为两档,这是设计阶段自我否决的第一处,第二处就是上面的打分出局——两个方案看起来都更智能,但推演到调试和成本层面就站不住了。真正被实践打脸的地方在后面。

2

3

决策二:循环——回答一次还是迭代到验证通过

这也是 Agent 系统和聊天机器人最本质的区别,是五个决策里最影响“能不能真正干活”的一个。

4

Agent 的核心是一个循环:接收任务 → 拆解计划 → 执行一步 → 观察结果 → 判断是否完成,没完成就继续推进。循环设计的第一性原理是可验证,即:模型的每一步判断必须建立在真实的执行反馈上,而不是它自己的想象上。代码到底报没报错、订单到底查没查到,这些信息都要原样回传给模型。模型一旦凭空判断“我觉得应该对了”,就会是 Agent 翻车的第一大原因。

循环要回答三个问题:什么时候算完成、失败了怎么办、长任务怎么不失忆。牛码的答案如下。

双保险终止

成功终止,考察验证命令是否通过。每个任务在计划阶段,必须声明机器可判定的验证命令(pytest tests/npm run buildcurl 健康检查接口),只要返回 0 即任务结束。不允许没有验证命令的任务进入执行阶段,这是个硬性条件。这会逼着 Agent 把“完成了”定义成机器可判定的东西,而不是看起来改好了。

强制终止,设置三道熔断闸门:

  • 最大迭代 30 轮。将上限设置为 30 轮,是为了在任务完成率和成本之间取得平衡:上限太低,复杂任务可能无法完成;上限太高,又容易导致成本失控。

  • 连续 3 轮无进展熔断。如果连续 3 轮工具调用结果与上一轮高度相似,即同一文件反复改同一处、同一报错反复出现。判定为原地打转,立即熔断并把当前状态汇报给用户。这条设计针对的是 Agent 失控时的一种常见情况:很少有任务真的会“走了 30 步还没走完”,更多时候是“从第 8 步开始原地打转”。但实践下来,这个设定一次都没被触发过。因为这里的判定逻辑是“工具调用与结果签名完全相同”,而模型每轮都会换个小花样(加 -v、加 echo、换个目录),签名轮轮不同,检测器就眼睁睁看着它转圈,最后是最大轮数兜的底(见下图)。这也提醒我们,如果相似度判定过严等同于形同虚设。

5

  • 单任务 token 预算。Agent 系统可以配置限额,超了直接停。无论什么时候,都要有成本熔断。

重试:三类失败三种策略

牛码将失败分成三类:环境瞬时错误、代码逻辑错误和计划性错误。三类失败分别采用直接重试、带完整报错重试和重新规划三种策略。

6

其中,第二类失败类型的关键点是要原样、完整回传报错信息,不做摘要。把长报错截成“测试失败,共 3 个用例未通过”看似省 token,但实际上剥夺了模型定位问题的全部原料,它只能瞎猜。完整的 traceback 才是让它一轮定位的东西。

第三类错误类型,是同一步骤改代码重试 2 次仍失败,就升级为计划性错误,触发重新规划。

上下文:三层压缩防失忆

循环跑到十几轮,上下文会超过模型窗口。这个系统将压缩分三层:

  1. 工具结果分级保留。最近 3 轮全量保留,而更早轮次中,成功的调用压成一行,失败的保留报错摘要。这里遵循两个原则:越近的信息保留得越完整,失败信息的保留优先级高于成功信息。

  2. 计划置顶。和计划相关的信息,包括:任务原始描述、当前计划、每步状态,永远要固定在上下文最前面,不要做压缩。Agent 最致命的失忆不是忘了细节,是忘了自己在干嘛

  3. 文件内容不进历史。读过的文件只留类似“读过 auth.py)(412 行)”这种记录,需要时再让 Agent 重新读。文件是随时可再取的外部状态,没必要在上下文里存副本——三层压缩机制中,这条省的 token 最多

7

决策三:工具——能力边界和权限边界要一起画

模型的能力边界,很大程度上取决于你给它接了哪些工具。但工具接得越多,项目风险越大。所以,接入工具的铁律是每接入一个工具,同步回答它的权限问题。我们要理清哪些操作会自动执行,哪些操作必须人确认,还有哪些操作要直接禁止。

如果先接工具再补权限系统,那么补权限的那天一般是出事的那天。

另一条反复被验证的原则是工具保持愚蠢,智能留给模型。工具层替模型摘要、替模型判断的每一处小聪明,后面都会变成模型判断失准的盲区。

牛码的工具层分两步走:本次实测的 MVP 阶段,出于够快、够验证循环设计的考虑,四个工具以本地函数直连实现。而生产化的目标路径是封装成 MCP 工具、通过七牛云自定义 MCP 托管服务接入——控制台 MCP 商店的预置工具已覆盖企业微信、联网搜索等通用能力,代码沙箱这类刚需需要自建接入。工具层目前包含四类能力:

  • 文件系统:读写限定项目根目录内,.env、密钥文件、系统路径默认拒绝

  • 代码执行沙箱:见下

  • Git 操作:MVP 未做独立工具,git 命令经沙箱执行,强制推送会被判为高危命令进行拦截

  • 部署:见决策四

沙箱用 Docker 落地,一个任务一个容器,任务结束即销毁。完整规格:

docker run --rm \
  --network none \
  --memory 2g \
  --cpus 2 \
  --pids-limit 256 \
  --read-only \
  --tmpfs /tmp:size=512m \
  -v /workspace/task-{id}:/workspace:rw \
  --user 1000:1000 \
  --security-opt no-new-privileges \
  niuma-sandbox:latest \
  timeout 300 bash -c "{command}"

上面的设计逻辑为默认断网是最大的杀手锏。绝大多数“跑代码、跑测试”的任务不需要网络,需要装依赖的场景有内部 npm/pip 镜像源的白名单网络。如果 Agent 生成的代码中混入外传的数据逻辑,断网会是最后一道保险。超时设置宁短勿长,这里的单命令上限为 5 分钟。超时的痛苦是显性的,跑飞的成本是隐性的。胖镜像要一次把常用运行时(Python、Node、Go、JDK)装够,代价是体积膨胀到 2GB 左右。反面教材是按语言拆小镜像,但因为任务经常要跨语言,切来切去的复杂度大于省下的磁盘。

本次实测用的是最小镜像,恰好撞出了这个反面教材。在某次运行中,镜像内缺少 pytest,模型又要执行 pip install pytest。由于是在 --read-only + --network none 的容器,运行时装依赖这条路行不通,最后只能改用 unittest 重写测试才完成任务(下图)。沙箱越严,镜像越要一次装够

8

工具侧封装很轻量,沙箱工具只暴露一个 run_command(command),返回 {exit_code, stdout, stderr} 三元组,零加工。

决策四:闭环——你的 Agent 的最后一公里是什么

这个决策是最容易被忽略,但又是自建系统真正的差异化来源。

每个领域的 Agent 都有一条通用工具到不了的最后一公里。通用 Coding 工具停在“代码写完、测试通过”,部署上线是人工的;通用客服工具停在“回复已经拟好”,发送和工单流转仍由人工完成;通用数据工具停在“报表已经生成”,后续进入决策流程仍要依赖人工。

自建 Agent 最大的价值,是把这最后一公里接进你自己的基础设施,让“我描述一个需求”到“这个需求已经生效”变成一条不中断的链路。设计这一层要想清楚三件事:生效动作失败了怎么回退、不同环境怎么隔离、Agent 在这一层该有多大权限

牛码的最后一公里是部署:编码完成、测试通过后,Agent 调用对象存储 Kodo 版本化上传构建产物、切换 current 指针、尝试刷新 CDN,这样用户拿到的是一个链接,而不是一堆改动过的文件。

9

图注:本次实测跑通了上传/版本化/切指针/健康检查/失败自动回滚;云主机部署未实现,测试域名被平台 CDN 访问控制拦截,公网可访问性未验证。

回滚:版本化切指针,不做撤销

部署工具上传产物到 Kodo 时不覆盖旧文件,按版本号写入独立前缀:

kodo://app-bucket/releases/v20260715-1432/   ← 本次
kodo://app-bucket/releases/v20260714-0910/   ← 上一版
kodo://app-bucket/current → 指向某个 release 前缀

上线是把 current 切到新版本 + CDN 刷新,而回滚是切回上一版 + 再刷新一次,不需要重新构建。部署后健康检查失败时,Agent 自动回滚,然后停下来汇报。这里回滚可以自动,但“回滚之后再试一次”必须由人来决定,否则会出现“部署-失败-回滚-再部署”的死循环。

多环境:物理隔离,不靠参数

测试和生产不共用 bucket、不共用云主机、不共用密钥,是完全独立的两套资源,部署工具也通过不同的凭证访问。区分环境不依赖命令里传 --env prod——参数会写错、会被模型幻觉,这里依靠凭证本身的权限边界:拿着测试环境的 key,物理上碰不到生产资源。

权限:生产的钥匙从头到尾不在 Agent 手里

部署工具的默认配置中只有测试环境凭证,生产凭证根本不在 Agent 可触达的范围内。

生产发布走独立流程:Agent 完成测试环境部署并验证通过后,生成发布请求(版本号、变更摘要、测试环境验证结果)。人在控制台确认后,由平台侧持有生产凭证的独立服务执行同样的切换动作。Agent 全程不接触生产 key。牛码 MVP 目前做到了“只配测试 bucket”这一层——用的仍是账号级 AK/SK,物理上仍能碰到其他资源。真正的凭证隔离要靠子账号 + 授权策略,这属于生产化待办。

这自然比单纯依赖“生产操作需要二次确认”更可靠。二次确认在防误操作,凭证隔离防的是模型被提示注入、被诱导之后的恶意操作。

Agent 系统的安全要按最坏情况设计:假设模型某一天会被外部输入操纵,你的架构还能不能兜住。

决策五:安全——不是第五个决策,是前四个决策的约束条件

把安全放在最后讲,不代表它最后做。它贯穿前四层,而且必须在设计阶段就定下来,上线后再补的安全都是筛子。前面各节已经把安全设计织在了各层里,这里收拢成五条不分领域的底线:

密钥集中托管,不下发到终端——API Key 散落在每个开发者本地的系统,泄露只是时间问题。敏感资源默认拒绝——黑名单保护密钥文件、生产配置、客户隐私数据。高危操作二次确认——删除、外发、资金变动类动作,自动化止步于此。生产与测试物理隔离——靠凭证权限边界,不靠参数。全量审计日志——每次工具调用留痕。

审计日志的格式也定下来,每条工具调用记一行 JSON:

{"ts": "2026-07-15T14:32:01+08:00","task_id": "t-8891","step": 7,"tool": "run_command","args_digest": "pytest tests/ -x","exit_code": 1,"duration_ms": 4210,"model": "deepseek/deepseek-v4-flash","tokens": {"in": 8412, "out": 356}}

两个设计点:args_digest 对超长参数只存摘要和哈希,日志不能变成第二个代码副本;每条带 modeltokens,审计日志同时就是成本账本,用来事后追溯“这个任务为什么花了这么多钱”。这样,就不用查第二个系统。

设计被实践修正的地方,才是最值钱的

回头对照最初的设计方案,有两处是设计阶段就自我否决的:三档路由简化为两档、复杂度自动打分被规则路由替代。

而真正被实践打脸的也有两处:连续 3 轮无进展熔断一次都没触发过(判定过严,形同虚设),以及“运行时装依赖”在断网只读的沙箱里彻底行不通(胖镜像不是可选项,是必选项)。

留下来的部分——循环吃真实反馈、工具保持愚蠢——被实测反复验证;生产凭证不进 Agent 这条本次没有生产环境可验,仍是设计原则。

设计方案在纸上永远是完美的,价值在于它被现实修正的那些地方。这也是我把这四处“推翻”原样写出来的原因:如果你在搭自己的 Agent 系统,这些弯路可以直接跳过。

全文速查表

一张表速览全文。建议收藏,搭自己的 Agent 时对着抄:

10

想亲手试试?

文中这套“模型 + 循环 + 工具”的能力,不用等你自己搭完才能感受。现在,七牛云 Agent 体验平台,多模态交互、工具调用、智能推理都可以在对话里直接体验——让它审查一段代码、调用工具完成一个任务,你就会对“循环吃真实反馈”这条原则有直观得多的体感。

体验完想动手搭自己的:我搭牛码用到的全部积木——协议兼容的大模型推理 API、对象存储与 CDN 的部署链路——都在七牛云 AI 控制台可以直接接入,我没有用到任何内部特权。一个人业余时间能搭出来的东西,你和你的团队没有理由搭不出来。这篇文章里的参考实现和参数,就是给你抄的。

posted @ 2026-07-22 14:22  小七-七牛开发者  阅读(21)  评论(0)    收藏  举报