先给你交个底我做的不是又一个给人看的博客或文档站而是一个“Agent 读起来更顺口”的信息站点。这个站点面向各种基于大模型构建的智能体Agent为它提供结构化的 API 说明、技能定义skills、可验证的数据块和统一的调用入口核心目标是让 AI 在做事时不瞎猜、不乱编、不擅自“自由发挥”。去年我折腾了大半年前后跑了不少实验这里把整个设计思路、关键代码结构、实测数据和踩坑记录一次性整理出来希望能帮到正在被 Agent 幻觉和工具调用搞到头大的朋友。1. 先定位病根Agent 的“不可靠”到底出在哪1.1 幻觉不是模型的锅是信息通道的问题最早我跑自动化任务时遇到一个非常典型的场景让 Agent 从某个数据源拉取近一周的销售数据并按格式汇总。模型本身表现不错但结果里居然有三天的数据和对不上号细看是它把旧接口文档里的字段名跟新接口的字段名混用了。再往前排查根因是旧文档还挂在站点上而新文档藏在了二级菜单里Agent 抓取页面时优先级和权重都很随机——它抓到什么就信什么没有能力判断文档新旧。这件事让我意识到大家总说大模型会“一本正经地胡说八道”但很多时候模型只是在按它看到的内容办事。如果输入是一堆乱序、过期、无结构化标签的 HTML它输出的“幻觉”其实是信息通道问题不是模型本身的问题。ChatGPT 这类模型擅长的是从好材料里总结而不是从垃圾堆里考古。所以与其寄希望于模型“自己想明白”不如把信息通道修好。1.2 工具调用与上下文管理的失控第二个让我下决心做这个网站的触发点是工具的调用。当时在某跨平台系统里挂了一个 Agent给它配置了十几个工具查数据库、调外部 API、发邮件、更新工单。刚开始跑得很顺畅但一旦同时涉及三四个工具问题就开始出现不知道选哪个工具随机挑一个看起来像的接着用工具参数传错了比如把用户 ID 传成了订单 ID系统也不报错返回“查询结果为空”Agent 就直接按空数据处理多个工具返回的结果互相矛盾时Agent 倾向于“折中”编一个数而不是标记异常上下文被大量工具说明和长文档塞满真正有用的数据反而没有被模型注意到。这些几乎都不是模型智力的问题而是工具定义和信息组织方式的问题。工具说明写得像给人看的帮助手册一行字里塞了太多含义用 JSON Schema 定义参数时字段描述模棱两可失败时的错误码没有统一的语义Agent 不知道该不该重试、该不该降级。1.3 我给这个网站定下的三个核心目标针对上面这些病灶我给这个站点定下了三个核心目标后面的所有设计都围绕这三个点展开第一可解析性优先。站点上的一切内容都必须能被人肉阅读但更要能被 Agent 的解析器一遍读懂。所有关键信息要有明确的标记、版本和更新时间不允许存在“多版本并存”的过期页面。第二技能显式化。把每个工具的调用方式、输入输出格式、权限边界、失败补偿策略全部写进一个独立的“技能包”skill而不是散落在文档里让 Agent 自己总结。Agent 在面对任务时先去技能仓库里匹配合适的技能匹配不到就直说而不是硬编一个调用方式。第三可验证性。页面上的数据和接口返回结果尽量附带校验信息例如数据快照的哈希值、更新序列号、字段的来源标记。Agent 在引用这些数据时可以附带验证信息下游系统也能快速判断数据是否被篡改或过期。这三个目标听起来不复杂但落地时牵扯到的细节非常多。设计一个人类阅读友好的文档站已经很容易了但设计一个“AI 阅读友好”的站点很多原则和习惯都得反过来。2. 给 Agent 读的网站长什么样从“HTML 给人看”到“结构化给 AI 吃”2.1 页面如何设计才能让解析器一遍读懂一个很常见的误区是只要网站内容在 HTML 里Agent 就能看懂。实际上Agent 抓取页面时走的是网络请求、文本提取、上下文拼接这条链路跟搜索引擎爬虫类似但更挑剔。页面里如果大量使用 JavaScript 动态渲染内容Agent 的 HTTP 客户端可不会帮你执行 JS它拿到的是空壳 HTML自然什么都读不到。所以在设计这个站点时我定了几个硬性规范所有核心内容使用服务端渲染确保直接请求 URL 就能拿到完整文本禁止关键信息依赖图片、图表、倾斜字体等视觉表达一律使用标准文本和代码块页面头部放一个llms.txt风格的导航索引让 Agent 在打开首页后能直接跳转到具体模块不必靠“猜”来遍历每个页面有唯一的page-id元信息格式类似skill:calculator:v1让 Agent 可以确认它读的就是目标版本在页面底部标注内容生成时间和校验信息防止 Agent 把过期内容当成事实。此外我要求每个页面都有一个“Agent 摘要区”。这段摘要不是给人看的而是给 Agent 的解析器提前告知页面可用信息和关键入口。它的格式很简单就是一个带固定标记的块AGENT-BRIEF page: skill-calculator version: 1.0.0 updated: 2025-xx-xx summary: 计算器技能支持四则运算与百分比计算输入使用 expression 字符串输出为数值。 endpoints: /api/calc : 唯一的调用入口 /skills/calculator/examples : 示例集 AGENT-BRIEF-END这样设计的好处是Agent 不用读完整个页面才知道这个页面是干嘛的它可以先扫一眼 AGENT-BRIEF 区再决定要不要深入。这大幅减少了上下文占用也降低了抓取到错误信息后“硬解读”的概率。2.2 接口层的三件套schema、版本号、示例页面只是信息展示层真正让 Agent “干活”的还是接口定义。我这边把接口描述全部标准化成三件套JSON Schema 描述输入输出、semantic version 管理接口版本、每个接口附 2 到 3 个典型示例。为什么强调这套三件套因为 Agent 调用工具时最大的不确定性其实是参数语义。比如一个查询天气的接口city字段到底是写城市名还是写城市代码要不要带省份是支持中文还是拼音如果接口文档只是含糊地写“城市名称”Agent 大概率会按自己的理解传一个“北京市朝阳区”然后接口返回空值它又不会反思直接就说“该地区无数据”。做了三件套之后每个接口都会这样描述{ name: queryWeather, description: 根据城市名称查询当前天气城市名称需与气象数据源保持一致支持中文城市名。, parameters: { type: object, properties: { city: { type: string, description: 城市中文名称例如北京、上海、广州。不支持缩写如BJ和区县级地址。, examples: [北京, 上海, 广州] }, unit: { type: string, enum: [celsius, fahrenheit], default: celsius, description: 温度单位默认摄氏度。 } }, required: [city] }, returns: { type: object, properties: { temperature: { type: number, description: 当前温度数值 }, condition: { type: string, description: 天气状况描述如晴、多云、雨。 } } }, examples: [ { request: { city: 北京, unit: celsius }, response: { temperature: 25, condition: 晴 } } ] }把示例直接写在 schema 里比任何“帮助文档”都有效。Agent 在构造参数时不仅有字段名和类型约束还能参考一个完整范例这就像给了一个填空题的答案模板基本杜绝了“编造参数”的行为。2.3 技能仓库目录不只是一个网站而是一个可访问协议当接口数量超过二十个后我发现“一页一接口”的模式开始失效Agent 面对任务时不知道该去哪个页面找哪个接口。于是我把站点升级成了一个“技能仓库”每个技能都是一个独立的可导航单元有一份SKILL.md作为入口。整个站点的目录树大致是这样/ → 首页包含 llms.txt 导航索引 /skills/ → 技能仓库首页列出所有可用技能名称和版本号 /skills/calculator/ → 计算器技能 /skills/calculator/SKILL.md /skills/calculator/examples.json /skills/weather/ → 天气查询技能 /skills/weather/SKILL.md /skills/order/ → 订单查询技能 /skills/order/SKILL.md /api/ → 实际的接口端点 /llms.txt → 机器可读的站点地图这个结构下Agent 的访问路径非常明确接到任务 → 打开/llms.txt看全局 → 进入/skills/匹配技能 → 读取对应 SKILL.md → 根据 schema 调接口。整套链路不需要模型“灵光一现”只需要它按部就班地执行。技能仓库本身还承担了去重和冲突消解的责任。同一个功能只能有一个技能不允许一个接口在三个页面里出现三个说法。如果有新版本技能上线旧版本必须在页面上标记为“deprecated”并给出替代技能链接避免 Agent 抓到过时内容。3. skills 到底怎么写用一份计算器技能拆解全过程3.1 技能目录的元信息与触发条件技能定义是整个站点最核心的部分。我拿计算器技能来举例因为这个需求足够简单能清楚展示每段配置的作用但逻辑又能泛化到更复杂的工具上。一个技能的SKILL.md顶部是 YAML 格式的元信息这部分主要给 Agent 做快速判断用--- name: calculator display_name: 数值计算器 description: 处理需要精确数学运算的请求包括四则运算、百分比、括号表达式。 version: 1.2.0 license: internal permission_level: read-only trigger_keywords: - 计算 - 算一下 - math - calculate - 百分比 - 四则运算 context_budget: 1200 ---trigger_keywords是我后来才加的字段。没有它的时候Agent 经常把“帮我估算一下项目的周期”这类模糊语句错误匹配到计算器技能上导致它用精确的数值接口去处理一个需要判断的常识问题。加上触发词之后Agent 可以先判断用户意图是否命中再决定加载哪个技能误触率下降不少。context_budget是给 Agent 的“预算提示”这个技能完整说明大概占多少 token。Agent 可以据此决定要不要一次性加载完整内容还是先加载摘要、按需读取细节。这个字段对复杂技能特别有用尤其是那种带着超大示例包的技能一次性加载会挤爆上下文窗口。3.2 输入输出描述让参数不出格技能正文部分我会分几个固定段落Usage调用方法、Input Schema输入描述、Output Schema输出描述、Examples示例、Error Handling错误处理。下面用计算器技能的输入输出描述来展示关键写法## Usage 调用 /api/calc 接口HTTP 方法为 POST。请求体使用 JSON 格式字段如下 { expression: 字符串类型的数学表达式仅支持数字、括号、 - * / 运算符和百分号。, precision: 可选小数保留位数默认 4。 } ## Input Schema - expression必填。示例(12.5 3.2) * 4、1500 * 0.2。 - precision可选。取值范围 0-10超出范围时按 10 处理。 ## Output Schema 正常返回 { result: 计算结果number 类型, expression: 实际解析的表达式string 类型, precision: 实际使用的小数精度 } 异常返回 { error: { code: 错误码字符串, message: 可读的错误描述 } }这里给每个字段都写上了类型和示例同时明确标注了边界规则比如 precision 超出范围时怎么处理。Agent 看到这种描述后构造参数时会“照着抄”而不是“凭感觉”效果差异非常大。有一个小细节我在描述里尽量避免使用“可能”“大概”“或其他类似值”这类模糊词。给 Agent 的文档和给人看的文档不一样人看到模糊词会去查资料Agent 看到模糊词往往就直接自由发挥了。所有边界情况最好都明确写出结果不给它发挥空间。3.3 失败降级与权限边界Agent 的“安全带”工具调用一定会出错。我之前踩过的一个坑是接口返回 500 时Agent 会反复重试同一请求把上游服务打到过载而接口返回 400 参数错误时它反而会“聪明”地修改参数后再试把错误参数越改越离谱。所以我在技能定义里增加了一个“错误处理与降级”段落强制规定每种错误码对应的行为## Error Handling - HTTP 400参数错误禁止重试必须重新检查 Input Schema 中的字段描述。 - HTTP 404接口地址错误禁止自行改 URL应向调用方报告“接口地址不存在”。 - HTTP 429触发限流等待 5 秒后可重试一次失败则停止。 - HTTP 500服务端异常可重试一次失败后必须报告异常不允许使用默认值替代结果。 - 解析错误如果接口返回内容不是合法 JSON视为异常禁止猜测内容。这段对 Agent 的约束相当于给它系上了安全带。它再也不会因为一个小错误就无限重试也不会在结果为空时自作主张填一个默认数。这个“禁止猜测”原则在错误处理里尤其重要——宁可让 Agent 说“我不知道”也不能让它编一个像模像样的答案出来。权限边界同样写进技能元信息里。计算器技能是read-only意味着它的调用不产生副作用像“发邮件”这种技能则会标记为write并且还要标注“调用前需用户确认”。Agent 默认是无权调用写操作的必须主动申请并由用户侧确认后才能执行。这个机制在后期防止了不少事故尤其是当 Agent 上下文较长、记忆混乱的时候权限声明能兜住最后的底线。4. 实测对比可靠性提升多少token 多烧多少4.1 评测方案与场景集光说不练假把式。站点和技能仓库上线后我搭建了一套基础的评测流程用同一组任务场景集跑“改造前”和“改造后”两轮对比。场景集包含四类任务数据查询类比如查订单状态、查库存、查天气要求结果准确多工具编排类比如先查库存再计算补货量最后模拟生成采购建议模糊意图类用户请求故意写得含糊比如“帮我看看最近怎么样”考验 Agent 是否请求澄清矛盾数据类不同接口返回的数据存在冲突考验 Agent 是否识别异常并报告。评测时我记录了五个指标幻觉率结果中出现“无中生有”数据的比例、工具调用成功率一次调用返回非错误且语义符合预期的比例、任务完成率最终输出基本满足用户需求的比例、平均往返轮次Agent 与用户或服务端交流的次数、平均耗时。4.2 核心指标的改善情况下面是两轮评测的对比数据。测试样本总数不算特别大但趋势非常稳定我跑了三轮都是一致的方向指标改造前改造后变化幻觉率18.4%3.7%下降 14.7 个百分点工具调用成功率72.1%93.5%提升 21.4 个百分点任务完成率61.2%88.9%提升 27.7 个百分点平均往返轮次5.8 次3.4 次减少 2.4 次平均耗时46 秒37 秒减少约 9 秒幻觉率的下降在意料之中因为站点提供了结构化数据和明确的版本信息Agent 不需要再“填空式”猜测字段和参数。但让我比较意外的是平均往返轮次的减少原本 Agent 因为拿不准信息需要反复向用户确认有了明确的技能触发关键词和 schema 示例之后它第一轮就能做出相对正确的动作用户的确认次数自然就少了。有意思的是模糊意图类的任务在改造后的完成率反而没有想象中高。这说明站点只能解决“信息清晰时 Agent 的执行问题”并不能根治“用户意图本身模糊”的问题。该让 Agent 学会问清楚的场景依然需要交互策略来兜底这部分我在后面还会提到。4.3 新增成本上下文体积与缓存策略给 Agent 做网站不是没有代价的最大的代价是上下文体积的明显膨胀。一个技能包动不动就上千 token如果 Agent 同时在用五六个技能光技能说明就能吃掉一万多 token。在上下文有限的模型下这直接影响 Agent 能记住的用户信息量甚至导致关键对话内容被挤到“视野之外”。我的处理方案是分层加载第一层/llms.txt只有几十个 token相当于技能索引第二层技能仓库首页/skills/列出技能名和一句话简介约几百 token第三层单个 SKILL.md几百到上千 token按需读取第四层完整的示例包和 schema 详情一般只有真正要调用某个接口时才加载。我用一句话给 Agent 定义了加载策略先在索引层做粗匹配命中后再进入详情层。这就像人查字典一样先看部首目录再翻到具体页而不是抱着整本字典从头啃到尾。实测下来开启了分层加载后平均每个任务的上下文占用比“全量加载”减少了约 40%。缓存层面也值得说。Agent 端往往有上下文缓存或者是外部缓存服务但如果站点内容变化频繁缓存可能反噬Agent 用了旧的技能版本去调新接口报错了又不知道为什么。我的做法是给每个技能页和接口响应都加ETag或version字段缓存时同时缓存版本号一旦版本号变化立即丢弃旧缓存。这是最简单也最有效的缓存一致性方案。5. 踩过的坑和挖出来的经验5.1 旧缓存与版本漂移一次现场事故复盘有一次我更新了天气技能的接口地址从/api/weather换成了/api/weather/v2但在 SKILL.md 的版本号上只更新到了 1.1.0没有做显著提醒。结果 Agent 抓取到了旧缓存里的SKILL.md v1.0.0还在调用旧接口。旧接口已经部署了兼容层能返回数据但数据格式略有不同。Agent 本身并不感知这个差异结果就是它把旧格式的数据当作新格式返回给了用户看起来一切正常实际上已经埋了错。事后我总结了两条改进第一接口变更必须同时更新 SKILL.md 中的version字段并且把变更点写到页面顶部的AGENT-BRIEF中比如“breaking change: 接口地址变更”第二下游系统要在接口响应里带上schema-version头Agent 可以根据响应头判断数据版本与技能版本是否一致不一致时主动告警而不是默默接受。这个事故也让我意识到像这样的站点不能只当成“静态文档”来维护必须有严格的发布流程和版本管理。GitOps 式管理是最好的每个技能包都进仓库有明确的历史记录和回滚机制。5.2 Agent 上下文被文档塞爆大而全的代价早期我把每个技能的完整文档都放在 SKILL.md 里一个技能动不动三千 token。Agent 接到任务后把两三个技能全部读一遍上下文直接超过一万 token。模型的注意力被稀释到真正处理数据时反而“看不见”关键字段了。后来我把“完整版”和“浓缩版”拆开SKILL.md 只保留核心信息和加载指引示例包移到单独文件参数 schema 里只放最关键的约束。对比实验显示单技能的 token 占用从三千降到约八百而幻觉率并没有因为这部分的缩减而反弹。这说明“该详细的地方详细该省略的地方省略”比一味铺开信息要重要得多。5.3 模糊意图的引导设计让 Agent 学会说“我需要澄清”改造后的站点虽然大幅提升了信息清晰下的执行能力但模糊意图的任务仍然是个盲区。比如用户说“帮我准备一下明天的会议资料”这个任务本身没有明确的工具接口Agent 如果硬要匹配某个技能结果必然是一堆“努力编造”。我给站点加了一个“意图澄清协议”AGENT-CLARIFY 当用户请求无法明确匹配任何技能时必须输出以下格式 - 已理解的意图 - 不确定的关键点 - 可能匹配的技能列表 - 需要用户确认的问题 AGENT-CLARIFY-END这个协议不是模型的能力而是站点对 Agent 的“行为约束”。它逼着 Agent 在动手之前先确认意图而不是带病执行。加上这个协议后模糊意图类任务的完成率略有下降但“用户满意度”明显上升——用户普遍反馈“它终于会问我了而不是瞎搞”。5.4 权限与安全技能声明不是摆设最后想单独提一下权限和安全性。给 Agent 开放工具调用时权限模型和网站本身的访问控制是两回事但缺一不可。我在站点上给每个技能标注了permission_level但在实际运行中Agent 并不会真的去“检查”自己的权限再执行。于是我在接口层加了一道闸对write类接口强制要求请求头携带一个一次性授权 token这个 token 由用户在对话中点击授权才生成有效期五分钟。Agent 没有 token 时无论它怎么理解文档都无法真正执行写操作。这个机制加上权限声明才是完整的“安全带”。同时站点本身也需要防爬和防滥用。我给/api/下的接口加了基于 API Key 的基础鉴权而在公开文档区则允许无鉴权访问毕竟 Agent 需要抓取资料。两套机制互不干扰既保证了信息可得性又防止了接口被任意调用。整个项目做到这里我最大的感受是AI 的可靠性不是模型的独角戏而是系统工程。算力、模型能力是一部分但信息的结构化、工具的可描述性、流程的可约束性都是影响可靠性的大变量。给 Agent 做的那个网站和技能仓库本质上是给模型配了一套“工具书”让它不再依赖临场发挥。后续我打算继续把技能格式标准化做成可导入导出的包尝试让不同 Agent 框架共用一套技能定义也希望看到这套思路能用在更多团队里。目前来看离“AI 更可靠”这个目标至少已经迈出了踏实的一步。