先说结论我给自己的 Agent 项目搭了一个内部的“工具站点”专门负责承载各种 skills让模型只做决策把具体执行全部交给这个站点来完成。实际跑下来Agent 在流程稳定性和输出格式正确率上提升非常明显。这篇文章就把这个站点的设计思路、核心实现、接入配置和踩坑过程完整拆开讲一遍想给 Agent 做可靠性增强的朋友可以直接参考里面的套路。如果你做过生产级 Agent 应用应该对以下场景不陌生模型指令理解错了、参数传反了、调完工具忘了带上下文、返回的 JSON 偶尔多一个逗号……模型能力再强也无法保证每次输出都严格遵守约定。我早期靠提示词反复强调“你必须注意格式”“请不要遗漏字段”效果有限后来换了个思路——与其让模型在对话里“记住怎么做”不如把能标准化的能力全部做成外部接口让模型查得到、调得动并且用程序兜底校验。这个“给 Agent 用的网站”就是在这样的背景下逐步成型的。这整套方案适合正在做 Agent、工作流自动化或 AI 工具集成的开发者阅读尤其是被模型输出不稳定折磨过的人。里面会涉及站点如何设计、skills 如何定义和注册、Agent 如何通过 API 调用、失败时如何降级以及我在真实环境中排查过的几个典型问题。1. 先想清楚Agent 的“可靠性”到底丢在哪里1.1 三个让 Agent 翻车的典型场景我先复盘几个实际遇到过的失败案例方便大家对“不可靠”这件事有具体感知。第一个场景是信息检索。我给 Agent 配过联网搜索的能力它需要自己提取关键词、访问页面、抽取内容。听起来很简单但模型经常在“提取正文”这一步跑偏把导航栏、广告文案甚至相关推荐都当成正文返回导致下游总结全是噪音。你告诉它“只要正文”它确实照做了但“正文”这个概念对模型来说太模糊它没有能力准确识别页面里的 DOM 结构。第二个场景是参数传递。Agent 调用天气查询接口时把城市名和日期填反了调用计算器接口时把两个运算数的顺序搞错了。更常见的情况是模型在生成 JSON 请求体时多了一个字段或者少了一个引号接口直接 400。这类问题在对话式评测里很难暴露因为人工对话时会自动忽略这些小瑕疵但自动化流程一跑就崩。第三个场景是状态丢失。多步 Agent 在执行任务时前一步的结果没有传给后一步比如先查了库存下一步却用一个没有库存数据的上下文去计算订单金额。模型在长对话中的注意力分布很不均匀中间结果容易被遗忘或混淆。这三个场景共同指向一个结论Agent 的可靠性瓶颈往往不在模型的推理能力而在它与外部世界交互时的规范化程度。模型擅长的是理解意图、拆解步骤不擅长的是精确记住格式、稳定执行重复操作。1.2 提示词约束的极限很多人遇到上述问题时第一反应是继续优化提示词。我试过很多种写法在 system prompt 里加格式示例、用 few-shot 展示正确调用、在模型输出后加一层校验重试循环……有效果但天花板很明显。提示词的问题是它本质上是一种“软约束”。模型在生成每个 token 时都在做概率采样提示词只是提高了正确格式的权重无法做到 100% 保证。尤其在长上下文中模型对早期出现的格式示例的记忆会逐渐衰减到后面就开始放飞自我。另外提示词越长模型对核心指令的注意力反而可能被稀释。一个塞满格式说明的 system prompt会让模型在“该调用哪个工具”和“怎么调用工具”之间产生混乱导致工具误触发率上升。我曾经遇到过 Agent 在一个几乎无关的对话轮次里突然去调用查询接口输出的 JSON 字段是上一轮任务的关键词明显是格式模板污染了意图判断。所以我后来把原则定为提示词里只保留策略性描述比如“什么时候该用这个技能”“用什么顺序调用”至于“接口长什么样”“参数怎么填”全部交给外部 schema 去约束。1.3 技能外置为什么能改善可靠性技能外置的核心变化是把“模型必须百分百按格式输出”这个不可能的要求变成了“模型只需要在候选列表里挑一个接口并传少量参数”这个相对简单的任务。拿刚才的天气查询来说。改造前模型需要自己组织完整的请求逻辑包括 URL、查询参数、超时设置一步错就全错。改造后我在网站里注册了一个名为“当前天气查询”的 skill它的输入参数只有两个城市名和日期。模型只需要在 JSON 里写{city: 北京, date: 2025-01-15}其他事情全部由网站内部处理。这带来的直接好处是模型需要生成的自由文本长度大幅减少出错的概率也随之降低。更重要的是接口的执行逻辑可以像普通程序一样测试、回归、加日志而不是每次都要靠肉眼检查模型输出。从更宏观的角度看技能外置也是一种“责任边界”的划分。模型负责判断“做什么”程序负责“怎么做”。判断是一个开放问题程序是一个确定性过程。把一个开放问题拆解成确定性步骤本身就是提高可靠性的核心手段。2. 整体设计一个“只做工具、不做聊天”的网站2.1 站点形态与功能划分我给这个站点定的形态是一个极简的 Web 服务不渲染复杂的聊天界面只提供三类页面技能市场、技能详情、调用日志。技能市场是所有已注册技能的列表页。每一张卡片显示技能名称、一句话描述、标签、调用次数和成功率。Agent 或人类开发者都可以通过这些信息快速判断该用哪个技能。技能详情页展示技能的完整信息包括输入输出 schema、调用示例、使用注意事项、最近变更记录。这个页面是给 Agent 看的“说明书”。我在实测中发现如果把 schema 和说明组织清晰模型选择技能的正确率会显著上升因为它在快速阅读中更容易找到关键信息。调用日志页记录每一次技能调用的请求参数、返回值、耗时、状态码和错误信息。日志是排查所有问题的入口。没有日志你面对的就是一个“不知道哪一步出错”的黑盒有了日志你能定位到具体是哪一次调用、传了什么参数、返回了什么结果。站点后端是一个普通的 HTTP 服务我用 Python 的 FastAPI 框架实现主要是因为开发效率高schema 声明方便而且对 JSON 序列化的处理很顺手。每个技能封装成一个独立的执行模块模块之间通过统一的入口函数注册到服务里。2.2 skills 的定义与注册机制一个技能在站点里的本质是一个“输入输出都有严格约束的函数”。我把每个技能的元数据统一存成一个 JSON 描述包含以下关键字段{ skill_name: current_weather, display_name: 当前天气查询, description: 查询指定城市当天的天气情况包括温度、风向、降水概率。, version: 1.2.0, tags: [weather, daily], input_schema: { type: object, properties: { city: {type: string, description: 城市中文名称}, date: {type: string, format: date, description: 日期格式 YYYY-MM-DD} }, required: [city] }, output_schema: { type: object, properties: { temperature: {type: number}, condition: {type: string}, humidity: {type: number} }, required: [temperature, condition] } }这段描述看起来简单但每个字段背后都有用意。description不仅是给人看的更是给模型看的。模型在选择技能时主要依赖 description 来决定是否使用所以这里应该写清楚“这个技能解决什么问题、什么场景下用”而不是只写技术参数。input_schema和output_schema则是执行阶段的硬约束。模型生成的参数必须通过 JSON Schema 校验校验不过就直接返回错误而不是带着错误的参数继续执行。这样可以在源头拦截掉很大一部分格式错误。注册机制上我采用了“装饰器 目录扫描”的方式。开发者在技能模块里给函数加一个register_skill装饰器注明技能名称服务启动时会自动扫描所有注册的模块并加载元数据。新增技能不需要改主程序代码只需要在技能目录里新增一个文件对维护而言方便很多。2.3 可靠性三层防线这个站点的可靠性不是靠某个单一机制保证的而是三层防线叠加的结果。第一层是 schema 校验。所有传入的参数先过 JSON Schema 校验器不合法就直接拒绝。这一层拦截的是格式问题包括字段缺失、类型错误、枚举越界、日期格式错误等。它把「模型输出非法内容」的概率降到了接近零因为非法内容根本不会进入执行环节。第二层是业务校验。schema 校验保证的是“形状正确”但某些业务层面的错误它拦不住。比如城市名传的是“北京市”但天气数据源只支持拼音区划代码这时就需要执行模块内部做一次映射和校验。我通常会在执行模块的入口写一个validate子函数专门处理这类业务规则。第三层是回退机制。即使前面两层都过了执行环境本身也可能出错比如第三方接口超时、返回数据为空、网络抖动。这时候站点会返回一个结构化错误对象并且附带一个fallback_hint字段告诉 Agent 接下来可以尝试什么替代方案。例如天气接口超时fallback_hint就提示“可尝试查询最近一次缓存数据”。这三层防线分别对应“格式、业务、环境”三个维度的不可靠因素。实际跑下来第三层虽然发生的频率最低但带来的价值最大因为它让 Agent 在异常情况下不至于完全卡死。3. 核心实现技能接口与执行引擎3.1 技能怎么定义在 FastAPI 里我会把技能的处理函数设计成统一的签名格式接收一个dict类型的参数返回一个dict类型的结果。这样无论技能内部逻辑多复杂对外接口始终是一致的便于封装成统一的 HTTP 入口。from pydantic import BaseModel from typing import Any, Dict import json class SkillRequest(BaseModel): arguments: Dict[str, Any] class SkillResponse(BaseModel): success: bool result: Dict[str, Any] None error: str None fallback_hint: str None所有技能的执行逻辑都放在executor.py这个模块里。执行器拿到模型传来的 arguments 后会按顺序完成三步操作找技能对应的处理函数、校验参数、执行并返回结果。# executor.py import importlib import logging SKILL_REGISTRY {} def register_skill(skill_meta: dict): def decorator(func): skill_name skill_meta[skill_name] SKILL_REGISTRY[skill_name] { handler: func, meta: skill_meta } return func return decorator def execute_skill(skill_name: str, arguments: dict) - dict: entry SKILL_REGISTRY.get(skill_name) if entry is None: return { success: False, error: fskill {skill_name} not found, fallback_hint: 请检查技能名称 } try: result entry[handler](arguments) return {success: True, result: result} except Exception as e: logging.exception(skill execution failed) return { success: False, error: str(e), fallback_hint: 可尝试分解子任务或更换其他技能 }这里的核心思路是执行器不与具体业务逻辑耦合它只负责调度和异常捕获。所有的技能处理函数被注册到SKILL_REGISTRY后执行器就能找到它们。3.2 技能如何注册和管理注册机制的实现分为两部分。第一步是定义技能元数据第二步是把元数据和对应的处理函数绑定。我习惯在每个技能目录下放一个skill_def.py里面维护技能的描述信息、schema 和说明。然后在handlers.py里写具体实现并用装饰器注册。# skills/weather/handlers.py from core.executor import register_skill from skills.weather.skill_def import META register_skill(META) def handle_weather(arguments: dict) - dict: city arguments.get(city) date arguments.get(date, today) # 实际业务逻辑查询天气数据源 ... return {temperature: 22, condition: 晴}站点启动时会扫描skills目录下所有子目录导入每个子目录下的handlers.py模块。装饰器在导入时自动执行把技能注册到全局SKILL_REGISTRY里。这里有一个经验技能描述不要写成功能列表而要写成“使用场景说明书”。比如“这个技能在用户想了解某城市天气时使用当用户提到‘热’或‘冷’时也可先查询该技能获取温度数据”。模型在技能选择时更依赖这样的描述而不是“该技能输入城市和日期输出天气信息”这类干巴巴的技术说明。3.3 执行器里的关键细节执行器并不复杂但有几个细节值得展开讲讲。第一个是超时控制。每个技能执行都设置了独立的超时时间默认是 5 秒。超时的调用会被中断返回结构化错误。这个设计可以避免某个技能内部卡死导致整个 HTTP 请求无限挂起进而拖垮 Agent 的主流程。import concurrent.futures def run_with_timeout(handler, arguments, timeout5): with concurrent.futures.ThreadPoolExecutor(max_workers1) as executor: future executor.submit(handler, arguments) try: return future.result(timeouttimeout) except concurrent.futures.TimeoutError: return {success: False, error: execution timeout, fallback_hint: 任务处理超时可稍后重试或简化请求}第二个是幂等控制。某些技能会触发外部副作用比如发送通知、创建订单这类操作。这类技能在调用时如果因为网络原因导致客户端超时重试很可能造成重复执行。所以我在定义技能时引入了一个幂等键机制调用方在请求头携带一个idempotency_key站点内部用这个 key 判断是否已经执行过。对 Agent 场景来说这个机制非常关键因为模型的重试行为是不可预测的没有幂等保护很容易产生脏数据。第三个是返回值统一封装。我要求所有技能处理函数最终返回的都是字典即使技能本身只产生一个字符串结果也要包一层{value: ...}。这个约定看起来多余但它能让下游的日志记录、缓存和鉴权逻辑统一处理不需要针对不同技能写不同的分支。4. 接入 Agent配置与联调实录4.1 让 Agent 知道有哪些技能接入 Agent 的第一步是让 Agent 知道这个网站里有哪些技能可以用。我的做法是写一个“技能发现”接口Agent 启动或每次对话开始时调用一次拉取所有可用技能的元数据列表。GET /api/skills返回结构是一个技能数组每个元素包含skill_name、description、input_schema。Agent 端拿到这个列表后把这些信息注入到上下文中作为模型选择技能的依据。这里有个实测心得技能列表不是越多越好。如果一次注入 30 个技能的描述模型很容易被无关技能干扰导致选错。我后来在站点端加了“按需发现”机制Agent 先根据任务关键词调一个粗粒度的搜索接口只返回相关的 5~6 个技能描述效果明显好于全量注入。4.2 一次完整调用链路Agent 调用技能时走的是标准 HTTP 流程但我们需要在 Agent 的提示词层面做一次封装让模型知道“我的上下文里有哪些技能、怎么调用”。下面是一次完整链路中的关键步骤第一Agent 收到用户请求比如“帮我查一下北京今天适合穿什么衣服”。模型判断这个问题需要天气数据于是它从上下文中找到current_weather这个技能并生成一个调用意图{ intent: call_skill, skill_name: current_weather, arguments: {city: 北京, date: 2025-01-15} }第二Agent 端把这段意图转换为 HTTP 请求curl -X POST https://your-site.example/api/skills/current_weather/execute \ -H Content-Type: application/json \ -H X-API-Key: sk-xxx \ -d {arguments: {city: 北京, date: 2025-01-15}}第三站点返回结构化结果{ success: true, result: { temperature: 7, condition: 阴天转多云, wind: 北风3级 } }第四Agent 把result包装成一条工具调用结果放回上下文让模型基于真实数据生成最终回答。这四步看着简单但每一步都有相应的失败分支需要处理。比如第一步模型生成的不是合法 JSON第二步请求超时第三步返回success: false第四步结果字段和模型预期不一致。我针对每个分支都写了对应的重试或降级逻辑这样整个链路才算闭环。4.3 降级与重试策略在实际运行中降级策略比重试策略更重要。我曾经犯过一个错误把所有失败都设计成重试结果在一个第三方天气接口持续故障的时候Agent 反复调用同一接口既浪费了 token 又拖慢了响应。后来我把策略调整为格式错误直接重试业务错误根据fallback_hint提示调整策略环境错误做最多一次延迟重试失败后就切换到降级方案。所谓降级方案是指当某个技能全部不可用时站点会返回一组替代建议。例如天气类技能不可用替代建议是返回最近一次缓存数据并明确标注数据时间如果缓存也没有则返回“暂无法获取天气信息建议用户查看天气 App”。让模型知道承认能力边界比强行编造一个天气数据要可靠得多。重试时要特别注意幂等。对于查询类技能重试无副作用但对于写操作类技能比如“发送邮件”“创建工单”重试必须携带幂等键否则同一个请求触发两次写操作后果很严重。我在站点端对所有带有写副作用的技能统一开启了幂等保护未携带幂等键的请求直接返回错误促使 Agent 端养成正确的调用习惯。5. 测试与效果观察5.1 构建离线验证集为了验证站点和技能定义的质量我按照“离线优先”的原则搭建了一套测试脚本。核心思想很简单准备一批模拟的 Agent 决策记录每条记录包含用户请求、技能调用意图、期望的参数值、期望的结果结构然后批量回放。验证集覆盖几个维度正常场景、边界场景、异常场景。正常场景是“用户明确要求查询某类信息”边界场景是“参数缺失、日期格式错误、城市名带别名”异常场景是“接口返回 500、超时、数据为空”。每个技能上线前都要跑通整个验证集。我设置了一个底线指标所有边界场景的失败率必须为 0。宁可让技能被迫拒绝请求也不能让技能带病运行。因为 Agent 场景下的错误会叠加、传播一个错误的返回值可能被模型当成事实写进最终答案比直接报错的危害大得多。5.2 Agent 端到端测试离线验证通过后还要做 Agent 端到端测试。我会模拟几种真实用户输入观察 Agent 是否会选择正确的技能、是否生成合法的调用参数、是否能基于返回结果继续推进任务。端到端测试的关键是记录“决策轨迹”。我在 Agent 端加了一层日志记录模型每次调用工具前的思考过程和调用后的结果摘要。这样一来出现问题的时候我可以回溯到具体是哪一步的上下文导致模型做出了错误选择。举个例子有一次测试中 Agent 连续三次调用天气技能返回的结果都相同最后模型却回答“明天有大雨”和返回结果的“晴”完全矛盾。通过决策轨迹日志我发现问题出在上下文拼接顺序上工具结果被放在了系统提示之前模型在生成最终回答时没有参考到最新结果反而被早期的示范数据带偏了。修复方式很简单调整上下文拼接顺序把工具结果放到模型即将处理的位置问题就消失了。5.3 上线后的数据表现站点上线一段时间后我开始关注几个核心指标技能调用成功率、参数校验通过率、Agent 任务完成率、平均调用耗时。结果里的一个数据让我印象很深参数校验通过率稳定在 98% 以上。也就是说模型生成非法参数的概率已经很低这主要归功于清晰简洁的 schema 与按需加载的技能列表。整体 Agent 任务完成率的提升大约在 15~20 个百分点这个数字未必精确但趋势是明确的技能外置之后模型不再需要分心去记接口细节可以把全部注意力放在意图理解和任务推进上。另外还发现一个副作用——端上 token 消耗明显减少。原来每个任务都需要在 system prompt 里塞大量工具说明和格式示例现在这些内容都被移到了站点端Agent 只需要在调用前按需拉取少量技能描述token 成本自然就下来了。6. 避坑记录与实践总结6.1 技能函数必须幂等技能函数不幂等是我踩过最深的一个坑。最初上线某个通知类技能时因为没做幂等保护Agent 在超时后重试了一次结果用户收到了两封重复邮件。这个问题在测试阶段很难暴露因为本地网络几乎没有超时只有到真实生产环境里才会出现。排查起来也费劲日志里两次请求时间间隔很短参数完全一致但对应了两封邮件。最后靠增加“本次会话唯一 ID”才把问题定位清楚。现在我在写任何技能时都先问一个问题这个函数如果被执行两次结果会不会异常会就必须加幂等设计。6.2 注意上下文污染站点返回的结果会被作为数据写回 Agent 上下文这个过程存在污染风险。特别是当技能返回的 JSON 带有嵌套结构时模型容易把字段名和值混淆导致后续决策错误。我发现一个有效缓解措施站点的返回值尽量扁平化。宁可多返回几个字段也不要层层嵌套。比如天气数据拆成temperature、condition、wind、humidity四个顶层字段模型处理起来比{metrics: {temp: 7, cond: 晴}}这类嵌套结构轻松得多。另一个措施是在返回值前加一行摘要让模型先看到关键信息再看到完整 JSON。比如result里多放一个summary字段“北京今天阴天转多云最高 8 度最低 0 度建议穿着毛衣或薄棉服”。这样模型在生成回答时可以直接引用摘要不必自己费力解读结构化数据。6.3 权限与限流给每个 Agent 客户端配置独立的 API Key 是基本操作。每个 Key 绑定技能白名单和调用配额避免一个客户端异常调用影响整个站点。限流方面我按技能等级做了区分。查询类技能允许每分钟 60 次调用写操作类技能每分钟最多 10 次涉及外部付费接口的技能单独设阈值。超限请求返回 429并附带一个结构化错误提示引导 Agent 稍后重试。这里有个实践细节限流的阈值要写在技能元数据里让模型能够看到。因为模型在规划任务时如果知道某个技能有调用限制就会更合理地安排调用次数而不是在一个循环里反复调用同一接口。6.4 日志是排查问题的第一手段我见过很多 Agent 项目出现故障后开发者第一反应是去改提示词而不是去看日志。这个做法效率很低。提示词只能解决策略层面的问题而大量故障发生在执行细节层面比如某个字段取值超出预期、某个技能在特定输入下触发了异常分支。站点端我记录了四类日志访问日志、技能调用日志、错误日志、审计日志。访问日志记录每次 HTTP 请求的方法、路径、状态码技能调用日志记录具体的参数和结果错误日志记录堆栈和上下文审计日志记录敏感操作。在日志方案上我建议采用结构化日志格式也就是 JSON 行格式。这样排查时可以直接用文本处理工具过滤也可以接入日志分析平台做告警。排查流程一般是先查技能调用日志找到失败调用再查错误日志定位代码问题最后用访问日志统计影响范围。最后的体会这个站点项目做下来我最大的感受是Agent 应用与其花大量精力去“调教”模型不如花精力把模型和现实世界之间的桥梁建设得更规范、更健壮。给 Agent 做的这个网站本质上是一座主动降低模型输出复杂度的桥。你可以从一个小技能开始尝试比如先做一个查询类的接口让 Agent 通过 HTTP 调用它然后再逐步扩展技能市场、加入幂等保护、完善降级策略。这条路走出来的可靠性提升是单纯堆提示词很难达到的。我自己用过之后现在已经把大多数格式化输出任务都移到这上面了之后如果遇到更复杂的能力扩展需求我也打算顺着这个思路继续往里加新技能。