
说实话我本地跑过的 Agent 项目不算少了但真正让我下决心做一套“给 Agent 用的网站”的是某个周五晚上的翻车现场。那是一个自动化发布任务Agent 前面十几步都干得漂亮结果在最后一步把预发环境的地址当成了正式环境而且因为日志里恰好出现了一行production字段它甚至“自我确认”了这个错误直接往下执行。等人工发现的时候发布流程已经跑完一大半。从那之后我意识到一件事Agent 不可靠很多时候不是模型智力不够而是整条运行链路缺少结构。没人告诉它“边界在哪”没人为它保存关键状态没有机制在错误发生后拦住它并让它回退。我做的这个网站本质就是给 Agent 补上这些结构——工具入口、流程模板、状态管理、失败恢复、过程回放外加一套 skills 机制把已经验证过的任务路径固化下来让 Agent 不再每次都“临场发挥”。这篇文章把设计思路、Skills 的落地方式、可靠性机制和部署踩坑都拆开讲清楚如果你也在被 Agent 的“不稳定”折磨应该能省不少时间。1. Agent 为什么总是不靠谱先把问题拆清楚1.1 目标漂移上下文一长就忘本Agent 干活最典型的翻车模式就是做着做着忘了自己最初要什么。对话历史一旦积累到上万 token模型对“当前目标”的注意力会被大量中间过程稀释掉。尤其是有日志、报错、工具返回的混合上下文时它很容易把“上一轮的操作”当成“本轮的指令”把“日志里的描述”当成“实际发生的事实”。我见过最离谱的一次任务要求从订单列表里提取异常订单并汇总原因Agent 在第三步拉取数据时返回了一个字段缺失的结果它不但没去检查缺失反而在后续推理里反复引用这个不存在的字段最终生成了一份结构完整但内容全错的报告。从外面看流程每一步都执行了中间结果却早就跑偏了。所以目标漂移的本质是模型没有内建“任务边界意识”它既不维护一份“当前目标清单”也不会主动核对“这一步结果是否符合预期”。如果外部不给它提供这些约束它就会在一堆 token 里失去方向。1.2 工具幻觉把不存在的函数说得像真的第二类问题是工具调用层面的。LLM 在生成工具调用时偶尔会“发明”一些不存在的函数名或参数这在工具数量多、参数结构复杂的系统里尤其明显。举个例子系统里明明只注册了search_orders(user_id, page, page_size)模型可能因为指令里出现过“查一下用户订单”就直接生成query_order(user_id)甚至get_user_orders_by_name(name)。参数值也经常是编出来的它会把示例数据里的用户 ID 当成真实 ID 传进去然后把返回结果当成真实数据继续加工。这类问题靠“提示词里写清楚工具列表”只能缓解不能根治。因为模型生成 token 是概率性的工具名称越相似、参数越多它混淆的概率就越大。真正可靠的做法是在它发起调用时用程序拦住它参数不符合注册表的 schema 就直接拒绝而不是把错误传递下去。1.3 错误放大小失误滚成大雪球如果说前两类问题还可以分别防御那第三类才是最可怕的——错误在步骤间被级联放大。Agent 是多步执行的每一步的输出是下一步的输入。如果中间某一步产生了错误结果而下游步骤没有校验能力那么这个错误会像滚雪球一样越滚越大。更糟的是模型有极强的“自洽倾向”一旦它自己生成了一个结论后面它会倾向于寻找支持这个结论的证据忽略相反的信号。这就是为什么很多 Agent 项目在跑长任务时错误率不是线性增长而是指数增长。要打破这种放大效应必须在关键节点插入“确定性校验器”——用代码判断结果对不对而不是让模型自己判断。这是我后来设计整个网站时最重要的原则。2. 给 Agent 用的“网站”到底长什么样设计与架构2.1 为什么不能只靠封装一组 API很多人觉得给 Agent 加能力就是多写几个 API 让它调。这话对了一半。API 解决的是“能不能调”但 Agent 实际需要的是“能不能被正确地发现、安全地调用、有效地校验、失败后能恢复”。一个给人用的 API 文档写清楚路径、参数、返回值就够了但一个给 Agent 用的工具平台还要回答几个程序调用从来不会问的问题这个工具应该在任务哪个阶段被调用如果参数格式和模型生成的有偏差谁负责修正如果工具调用失败了是重试还是换方案调用结果要不要作为事实写入下一步的上下文这些决策如果都丢给模型现场判断就等于让列车在没有轨道的情况下自己找路。所以我做的这个网站本质是一个中间层。它对外暴露给 Agent 的是一整套“可发现、可校验、可观测、可恢复”的数字工作台而不是一堆裸接口。2.2 核心部件注册中心、运行时网关、状态服务、回放中心、Skills 库整个系统我拆成了五个部件各自职责很清晰部件职责核心存储/技术工具注册中心维护所有 Agent 可调用的能力清单、参数 Schema、触发场景PostgreSQL 索引运行时网关统一接收 tool_call做鉴权、参数校验、结果结构化、重试与熔断Python 异步服务任务状态服务保存会话快照、幂等键、断点信息支持回退恢复PostgreSQL Redis观测回放中心记录每次请求、响应、上下文支持事后还原执行过程对象存储 检索服务Skills 库存储 SKILL.md 与配套脚本按任务场景自动装配Git 仓库 文件系统工具注册中心和运行时网关是 Agent 的“手”决定它能不能正确操作任务状态服务和回放中心是 Agent 的“记忆”决定它会不会失忆、犯错后能不能复盘Skills 库是 Agent 的“经验”决定它能不能按已验证的路径执行而非每次碰运气。2.3 一次工具调用怎么穿过整条链路我把一次完整的 tool call 拆成六个步骤任何一个环节不过就直接中止或降级LLM 生成一个结构化的工具调用请求比如请求获取服务器状态运行时网关接收请求去注册中心核验工具是否存在、参数类型是否合法网关检查该请求是否携带有效的调用 ID 和会话上下文快照没有就直接拒绝网关通过适配层真正执行底层能力——可能是 HTTP 调用、Shell 命令或内部服务执行完成后网关把结果转成统一格式做字段完整性校验生成结果指纹结果返回给 LLM同时完整写入回放中心。{ tool_call_id: tc_7f3a2d9e, tool_name: get_server_status, arguments: { server_id: web-prod-01 }, execution_id: exe_20240119_001, session_id: sess_20240119_abc }一次看似简单的调用背后至少做了三类校验工具是否存在、参数是否符合 schema、会话状态是否有效。这些校验极大减少了前面说的“工具幻觉”和“错误放大”。3. Skills 才是这个项目的灵魂把经验固化成可复用流程3.1 为什么要用 SKILL.md 而不是“靠模型自己摸索”长任务里最让我头疼的是同一个任务Agent 每次的走法都不一样。有时候它先做 A 再 B有时候先 B 再 A有时候干脆乱序。结果是同样的需求今天成功、明天失败完全不可控。Skills 机制就是来治这个病的。把一个已经被验证过的高质量任务路径固化成一个资产包含触发条件、执行步骤、校验规则和兜底策略。以后 Agent 遇到同类任务不再现场推理“该怎么做”而是加载这个 Skill按里面的流程填参数、执行、验证。我自己的体会是Skill 的价值不亚于一个资深工程师把一个复杂流程写成标准作业指导书只是这份指导书是给模型看的。它把公司业务里那些“老员工知道但新人不知道”的流程知识变成了 Agent 可以直接使用的确定性资产。3.2 SKILL.md 的核心字段设计我设计的 SKILL.md 用 YAML front-matter 加正文描述组成核心字段就几个--- name: code-review-guard description: 在合并请求上进行代码审查重点检查密钥泄漏、危险函数和 SQL 注入 trigger: scene: code_review when: 用户要求审查代码或检测到 merge request 事件 inputs: - name: branch required: true description: 待审查的分支名 - name: base required: false default: main description: 对比基线分支 workflow: - step: fetch_files command: git diff --name-only {{base}}...{{branch}} - step: secret_scan tool: secret-scanner target: ${fetch_files.output} - step: llm_review tool: llm-reviewer target: ${fetch_files.output} verification: - assert: scan_result contains scan_summary - assert: risk_list is not empty fallback: - condition: secret_scan failed action: 降级为只返回变更文件列表并说明扫描失败原因 ---有几个设计细节值得说。第一trigger字段必须写得“可判定”不能用模糊场景词。比如“当用户需要代码质量分析时”就很模糊应该写成“用户要求审查代码或检测到 merge request 事件”这样自动装配器才能稳定匹配。第二workflow里每个步骤尽量引用上一步的输出用${step_name.output}这种显式引用。这能降低模型“自行决策”的空间流程路径越确定结果越稳定。第三verification字段是关键中的关键。它定义了步骤执行完以后系统要用代码校验哪些条件比如“结果里必须包含scan_summary字段”。如果校验不过整个 Skill 直接进入fallback。这个设计让“检查”从模型的任务里剥离出来交给了确定性的程序。3.3 自动装配Agent 怎么找到并加载合适的 SkillSkills 库里的技能越来越多之后另一个问题来了怎么让 Agent 在正确的时机选择正确的 Skill我的做法是两层匹配。第一层把每个 SKILL.md 的description和trigger.when做向量化任务进来时先用语义相似度筛出多个候选 Skill。第二层把候选 Skill 的trigger条件交给一个轻量分类器做判定选出最匹配的一个或几个然后把它们的执行模板注入到当前任务的上下文里。def assemble_skills(task_description: str, agent_context: dict): candidates vector_search( indexskill_index, queryf{task_description} {agent_context.get(scene, )}, top_k5 ) matched [ skill for skill in candidates if rule_match(skill.trigger.when, agent_context) ] if not matched: return None # 注入执行模板而不是让模型自由发挥 return render_skill_template(matched[0], agent_context)注入的时候有个小技巧不要一次性把所有 Skill 内容都塞给模型只要把匹配到的那个 Skill 的 workflow 和 verification 渲染成一段结构化指令喂进去让模型“照着填参数”而不是“重新发明流程”。实测下来任务执行的路径稳定性有明显提升。3.4 版本管理与回归测试技能更新不能靠肉眼Skill 不是写一次就完事的。业务逻辑会变、工具接口会变Skill 也必须持续迭代。但我见过太多团队改完 Skill 不验证结果新 Skill 上线后任务失败率反而更高。我给每个 Skill 配了一个测试集里面的用例都是历史上真实任务改造而来的输入固定、期望输出固定。每次 Skill 更新自动跑一遍回归测试看每个 step 是否按预期执行、verification 是否通过。只有通过率达标才允许发布新版本。skill-cli test code-review-guard --version 2.3.0 # 用例: 6/6 通过, 平均执行时间下降 18%这套机制还带来一个额外好处新人接手业务时不需要翻各种文档只要看 Skills 库的测试用例就知道这个技能在什么场景下应该产出什么结果。这本身也是组织经验沉淀的过程。4. 可靠性不能靠提示词要靠机制4.1 结构化输出把模型的自由发挥关进笼子里大模型的自由文本输出是 Agent 不稳定的根源之一。同一个意思它可以表达成十种不同的格式哪怕你在提示词里要求“必须返回 JSON”它也可能多包一层嵌套、把数字写成字符串、或者漏掉字段。我的做法是对每个工具的执行结果定义一份 JSON Schema网关强制校验。校验不过结果不会被当作事实进入下一步。{ type: object, required: [order_id, status, amount], properties: { order_id: { type: string, pattern: ^ORD-[0-9]{6}$ }, status: { type: string, enum: [pending, paid, failed, refunded] }, amount: { type: number, minimum: 0 } } }Schema 校验之外还要加一层语义校验。比如amount是数字、非负status必须是枚举值之一日期字段必须能被解析为真正的日期。模型可能结构上完全满足 Schema但语义是错的比如金额为负数但类型是数字这一层就是最后的防线。4.2 幂等、重试与熔断不能让错误被重复执行Agent 的另一个常见问题是重试导致的副作用放大。模型发现工具调用失败后很可能以同样的参数重发同一个请求。如果这个请求有副作用——比如扣费、发通知、创建订单——那一次错误就是多次损失。所以网关层实现了三个机制幂等键每个工具调用请求必须携带唯一的execution_id网关在同一键下只执行一次后续重复请求直接返回第一次的结果指数退避重试对网络类错误按 1s、2s、4s、8s 的间隔重试最多 4 次且只对安全错误类型重试熔断器某个工具连续失败超过阈值后网关直接标记该工具不健康后续请求不再调用它而是返回“工具不可用请切换方案并通知人工”。这三个机制的组合本质上就是把支付系统里那套“不可能让用户被重复扣款”的思路搬到了 Agent 世界里。async def execute_with_protection(call: ToolCall): cached await load_execution_result(call.execution_id) if cached: return cached for attempt in range(4): try: result await adapter.execute(call) await save_execution_result(call.execution_id, result) return result except RetryableError as e: await asyncio.sleep(2 ** attempt) except FatalError: circuit_breaker.mark(call.tool_name) raise circuit_breaker.mark(call.tool_name) raise ToolExecutionError(重试次数耗尽已熔断)4.3 状态快照与回放出错以后能“倒带”复盘我以前最痛恨的就是 Agent 出错后不知道它当时看到了什么、想了什么、做了什么。上下文被新的对话覆盖日志里只有零散的 tool call根本拼不出完整过程。状态快照机制解决了这个问题。系统在每个关键节点保存一份结构化的session_snapshot{ session_id: sess_20240119_abc, step: 12, current_goal: 发布构建产物到预发环境, last_result: { tool: deploy_release, status: failed, error: target_env is not allowed }, context_summary: 已完成代码拉取、构建、权限校验尚未执行发布, pending_actions: [确认目标环境后重试发布] }这份快照有两个用途。第一是回放定位出问题后运维同学可以查看每个步骤的快照知道 Agent 是何时偏离目标的。第二是恢复执行发现 Agent 跑偏后让它“回到第 12 步的状态重新来”而不是从头执行整个任务。前者解决“可追溯”后者解决“可恢复”。4.4 人工介入通道机器兜不住时人能接管最后一个可靠性机制听起来最不“智能”但实战中可能最有用——给 Agent 留一个人工接管的口子。我的做法是任务状态服务里为每个会话保留“暂停/接管”状态。当校验器连续失败、熔断触发、或者模型自己标记“不确定”时任务不再继续死循环而是转入人工处理队列。人工可以在工作台上直接查看快照、修改当前步骤状态、甚至绕过模型直接执行某个工具然后再把控制权交还给 Agent。这个设计让“可靠性”从模型单点承担责任变成了“模型机制人工”三层兜底。真正可靠的系统不是不出错的系统而是出了错能被人发现、被人接住、可以回退的系统。5. 部署、压测与踩坑实录5.1 部署形态与资源预估这个网站最初我用 Docker Compose 部署在一台 8C16G 的服务器上服务内部团队几十个 Agent 任务完全够用。核心组件就四个容器网关服务、PostgreSQL、Redis、对象存储网关。等日志量上来了再考虑迁移到 K8s 做自动扩容。资源预估方面我自己的经验是20 个并发 Agent、每次任务调用链平均 30 秒、每秒约 5 个 tool call 的场景下单机部署几乎没有压力。真正的瓶颈反而在日志存储和数据库大小而不是 CPU。5.2 五个让我印象最深的坑坑一SQLite 并发写锁导致接口间歇性失败。最开始为了省事我用 SQLite 当主库结果并发一上来频繁出现database is locked。这不是 Agent 的问题是我基础选型太轻率。后来换到 PostgreSQL配合连接池问题彻底消失。只要是并发场景别用 SQLite 做主存储。坑二完整日志导致磁盘爆炸。我把每次请求的完整上下文都存下来以为这样最保险。结果运行一个月单日日志量轻松到几十 GB。后来改成分级存储请求摘要、工具名、执行状态、耗时这些结构化字段永久保存完整的输入输出原文只保留 7 天再老的数据做抽样留存。回放排查基本用不上太久远的原文。坑三工具清单太长把 token 预算吃光了。工具注册中心里的能力越来越多每次请求前我习惯性把所有工具描述都注入给模型。然后发现模型还没开始干活prompt 就已经三四千 token遇到长任务直接在上下文窗口边缘崩溃。解决办法是按任务场景做工具预筛只注入与当前任务可能相关的工具描述其他工具留在注册中心供动态查询。坑四切换模型后 Schema 不兼容。同一个工具返回结果不同模型对格式的处理有细微差别有的模型会把数字字段自动转成字符串有的会把嵌套对象拍平。我一开始在网关里就固定了一套解析逻辑结果换模型后大量解析失败。后来把 Schema 校验独立成层并在网关做类型强转才稳定下来。坑五Agent 循环重试打爆下游服务。这个坑最隐蔽。Agent 遇到工具失败后会自己重试而且重试时可能带相同的参数。如果没有幂等层一个下游系统可能在几秒内被同一个失败请求打几十次。加上执行 ID 和结果缓存之后才算真正兜住。重试保护不是写给模型看的是写给下游系统看的。5.3 实测效果在我自己的环境里跑了一个多月的对比样本不算大只代表我这边的工作负载引入 Skills 机制之后计划类任务的一次性成功率大约从 58% 提升到了 87%加了幂等和熔断之后任务重试次数下降了六成左右而状态快照配合回放中心让我定位一个 Agent 失败问题的平均时间从原来的小时级降到了分钟级。这几个数字不算惊艳但对我来说已经足够说明问题Agent 的可靠性从来不是模型单点决定的而是被工程结构一层层托起来的。最后说一点个人体会。这个项目做下来我最大的收获不是“我会写代码了”“我会设计架构了”而是理解了可靠性的本质——它不是一个功能是一整套护栏。模型是概率系统它一定会犯错我们能做的不是求它别犯错而是设计一个系统让它犯错时能被发现被发现时能被控制被控制时还能回退。这个思路适用在 AI Agent 上其实也适用在所有复杂软件工程里。项目本身还能往很多方向扩展比如把 Skills 开放成团队共享库、接入更多验证器、把回放中心做成自动化调试工作台我后面还会继续折腾。