ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

面向智能体的信息站点设计:技能包与API的可靠性实践

面向智能体的信息站点设计:技能包与API的可靠性实践

先给你交个底:我做的不是又一个给人看的博客或文档站,而是一个“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 更可靠”这个目标,至少已经迈出了踏实的一步。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进