ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

为Agent构建专属网站:用Skills机制提升AI任务可靠性

为Agent构建专属网站:用Skills机制提升AI任务可靠性

1. 为什么我要给 Agent 单独做一个网站

先说说我做这件事的起因。过去大半年,我一直在折腾各种 Agent 项目,从最简单的任务型助手到稍微复杂一点的多步骤工作流,踩的坑不算少。最让我头疼的不是模型能力不够,而是Agent 在真实任务里“不靠谱”——同一个指令,今天跑得通,明天就翻车;明明给了工具,它偏偏不用;该查资料的时候它硬编,该动手的时候它空谈。

后来我意识到一个问题:我们一直在优化模型本身,却很少认真思考Agent 到底需要一个什么样的“工作环境”。人类员工需要工位、文档、工具、流程规范,Agent 也一样。它需要的不是一堆散乱的 API,而是一个结构化的、可被理解的、有明确边界的操作界面。

所以我动手做了一个专门给 Agent 用的网站,核心思路就一句话:把 Agent 需要的能力,用它能理解的方式,放在它能找到的地方。这个网站不是给人看的漂亮页面,而是给 Agent 读的“操作手册 + 工具箱 + 检查清单”三合一。里面包含了一套我称之为skills的机制——每个 skill 就是一个独立的能力单元,有明确的输入输出定义、使用条件和失败处理方式。

这个项目适合谁看?如果你正在做 Agent 相关的开发,不管是任务型助手、自动化工作流还是多 Agent 协作系统,这套思路都能直接参考。如果你只是对 AI 可靠性感兴趣,也能从中看到一些工程化的解决思路。我不打算讲太多虚的,直接拆开来讲我是怎么设计的、为什么这么设计、以及实际跑下来效果怎么样。

2. 整体设计思路:把 Agent 当“新员工”来对待

2.1 核心问题:Agent 为什么不可靠

在动手之前,我先花了两周时间复盘之前所有翻车的案例。把日志翻出来一条条看,最后归纳出三类高频问题。

第一类是能力边界模糊。Agent 不知道自己能做什么、不能做什么。你给它一个任务,它要么过度自信地硬上,要么过度保守地反复确认。比如我让它“整理一下这份数据”,它有时候会直接编造数据,有时候又会反复问我“你指的是哪份数据”。问题出在:我没有明确告诉它当前环境下有哪些工具可用、每个工具的适用条件是什么。

第二类是上下文污染。Agent 在长对话里会逐渐“忘记”自己原本的约束条件。一开始你告诉它“不要执行删除操作”,聊了二十轮之后它可能就把这事忘了。这不是模型的问题,是信息架构的问题——关键约束没有放在它每轮都能看到的地方。

第三类是失败后不会恢复。工具调用失败了,Agent 要么直接放弃,要么用同样的方式重试三次然后卡死。它不知道失败之后应该换什么策略,因为没人告诉过它。

这三类问题归结起来就是一句话:Agent 缺少一个稳定的、结构化的、始终可见的操作环境。模型能力再强,环境不对,照样翻车。

2.2 设计原则:三个“必须”

基于上面的复盘,我给自己定了三条设计原则。

第一,所有能力必须可发现。Agent 不需要“记住”自己有什么工具,它应该能随时查到一个完整的、最新的能力清单。这就像新员工入职时拿到的员工手册,不需要背下来,但要知道去哪查。

第二,所有约束必须可验证。不能只是口头说“不要做 X”,而是要在环境层面让 X 根本做不了,或者做了之后立刻能被检测到。约束要变成可执行的检查项,而不是一句自然语言提醒。

第三,所有失败必须可恢复。每个 skill 都要定义清楚:什么情况下算失败、失败后应该尝试什么替代方案、重试的上限是多少、超过上限之后怎么优雅退出。Agent 不需要“聪明”地处理所有异常,它只需要按预案执行。

这三条原则听起来简单,但落地的时候每一个都涉及大量细节决策。下面我逐个拆开讲。

2.3 为什么是“网站”而不是“插件”或“SDK”

有人问我为什么不直接做一个 SDK 或者插件,而要做一个网站。这个问题我想过很久,最后选网站的原因有三个。

第一,网站是语言无关的。不管你的 Agent 是用 Python 写的、JavaScript 写的还是别的什么技术栈,它只要能发 HTTP 请求就能用这套东西。SDK 会绑定语言和版本,维护成本太高。

第二,网站天然支持动态更新。我今天加了一个新 skill,明天改了一个约束条件,Agent 下次请求就能拿到最新的。不需要重新安装、不需要重启服务。这对于快速迭代的阶段特别重要。

第三,网站可以被“阅读”。这一点很关键。Agent 本质上是在做文本理解和生成,一个结构清晰的网页对它来说就是一份可读的文档。我可以把 skill 的定义、使用示例、注意事项全部用自然语言写清楚,Agent 直接读就行,不需要额外的解析层。

当然,网站也有缺点,比如网络延迟、需要处理认证、可能被滥用。这些后面会讲我怎么解决的。

3. 核心机制拆解:Skills 到底怎么设计

3.1 一个 Skill 的完整结构

每个 skill 在我的系统里是一个独立的结构化文档,包含以下字段。我用一个“查询天气”的假想 skill 来举例说明。

{ "skill_id": "get_weather", "name": "查询天气", "description": "根据城市名称查询当前天气状况", "when_to_use": "当用户询问某个城市的天气、温度、是否下雨等问题时使用", "when_not_to_use": "当用户询问历史天气或未来超过7天的预报时,不要使用此skill", "input_schema": { "city": "string, 城市名称,如'北京'", "unit": "string, 可选,'celsius'或'fahrenheit',默认'celsius'" }, "output_schema": { "temperature": "number", "condition": "string", "humidity": "number" }, "examples": [ { "input": {"city": "上海"}, "output": {"temperature": 22, "condition": "多云", "humidity": 65} } ], "failure_handling": { "city_not_found": "提示用户城市名称可能有误,建议使用标准城市名", "service_unavailable": "等待3秒后重试一次,仍失败则告知用户服务暂时不可用" }, "max_retries": 1 }

这个结构里,我认为最重要的是when_to_use 和 when_not_to_use这两个字段。大部分工具定义只告诉 Agent “这个工具能做什么”,但没告诉它“什么时候不该用”。结果就是 Agent 在不该用的时候用了,或者该用的时候没想到。

举个例子,我之前有个“搜索网页”的 skill,Agent 经常在明明知道答案的情况下还要去搜一下,浪费时间和 token。后来我加了 when_not_to_use:“当问题涉及常识性知识且你已有把握时,不要使用此 skill”。翻车率立刻降下来了。

3.2 为什么用自然语言而不是纯 JSON Schema

你可能会注意到,我的 skill 定义里大量使用了自然语言描述,而不是严格的 JSON Schema。这是故意的。

纯 JSON Schema 对机器友好,但对 Agent 不友好。Agent 需要理解的是意图和边界,而不是字段类型。比如"city": "string"只告诉了 Agent 类型,但没告诉它“城市名称要用标准中文名,不要用拼音,不要用缩写”。这些信息必须用自然语言写清楚。

我的做法是:结构用 JSON 保证可解析,内容用自然语言保证可理解。两者结合,Agent 既能程序化地读取字段,又能语义化地理解含义。

3.3 Skill 的版本管理与废弃机制

Skill 不是一成不变的。我经常需要修改某个 skill 的行为,或者废弃一个不再使用的 skill。这里有个坑:如果直接修改或删除,正在使用旧版本的 Agent 会突然失效。

我的解决方案是给每个 skill 加版本号,并且保留旧版本至少 30 天。Agent 请求时可以指定版本,也可以不指定(默认拿最新版)。如果某个 skill 被废弃,我会在返回里加一个deprecated标记,并附上替代 skill 的 ID。

{ "skill_id": "old_search", "deprecated": true, "deprecated_message": "此skill已废弃,请使用 new_search 替代", "replacement_skill_id": "new_search", "sunset_date": "2025-12-31" }

这样 Agent 在调用时如果发现废弃标记,可以自动切换到新 skill,或者至少能给用户一个明确的提示,而不是莫名其妙地失败。

3.4 权限与安全边界

Agent 能做的事情必须有限制。我在 skill 层面加了两层控制。

第一层是分类标签。每个 skill 属于一个或多个类别,比如read_only、write、external_api、sensitive。Agent 在初始化时会拿到一个允许的类别列表,只有匹配的 skill 才会出现在它的可用清单里。

第二层是调用配额。每个 skill 可以设置单位时间内的最大调用次数。比如“发送邮件”这个 skill 限制每小时最多 10 次,防止 Agent 陷入循环疯狂发邮件。

这两层控制都是在服务端做的,Agent 无法绕过。我踩过的坑是:一开始只在 prompt 里写“不要频繁调用”,结果 Agent 该疯狂调用还是疯狂调用。约束必须做在环境层面,不能只靠提示词。

4. 实操落地:从零搭建这套系统

4.1 技术选型与基础架构

我用的是最朴素的技术栈:一个轻量 Web 框架提供 HTTP 接口,一个 JSON 文件存储 skill 定义,一个简单的内存缓存做配额控制。没有用数据库,没有用消息队列,没有用微服务。

为什么这么选?因为这个系统的核心价值在于 skill 的设计质量,而不在于技术架构的复杂度。我见过太多项目在基础设施上过度投入,结果核心内容一塌糊涂。我的原则是:能用文件就不用数据库,能用内存就不用 Redis,能单机就不分布式。

具体来说,我的目录结构是这样的:

agent-site/ ├── skills/ │ ├── get_weather.json │ ├── search_web.json │ ├── send_email.json │ └── ... ├── config/ │ ├── categories.json │ └── quotas.json ├── server.py └── README.md

每个 skill 一个 JSON 文件,修改起来直观,版本控制也方便。categories.json定义了所有类别和默认权限,quotas.json定义了每个 skill 的调用限制。

4.2 核心接口设计

整个系统只暴露三个核心接口,保持极简。

第一个接口:获取 skill 清单。

GET /skills?categories=read_only,external_api

返回当前 Agent 有权使用的所有 skill 的摘要信息,包括 skill_id、name、description、when_to_use。不返回完整的 input_schema,减少传输量。

第二个接口:获取单个 skill 详情。

GET /skills/{skill_id}

返回完整的 skill 定义,包括所有字段。Agent 在决定使用某个 skill 之前,先调这个接口拿到完整信息。

第三个接口:记录调用结果。

POST /skills/{skill_id}/feedback { "success": true, "duration_ms": 230, "error_type": null }

这个接口看起来不起眼,但非常重要。它让我能追踪每个 skill 的实际使用情况:哪些 skill 经常失败、哪些 skill 从来没人用、平均响应时间是多少。这些数据是后续优化的依据。

4.3 让 Agent 学会“查手册”

系统搭好了,下一个问题是:怎么让 Agent 知道要去查这个网站。

我的做法是在 Agent 的系统提示词里加一段固定说明,大意是:“你有一个可用的技能库,地址是 XXX。在执行任何操作之前,先获取技能清单,确认你有可用的工具。不要凭记忆使用工具,因为技能库可能已经更新。”

这段话的关键是最后一句:不要凭记忆使用工具。我之前的 Agent 经常“以为”自己有某个工具,结果调用了一个不存在的接口。加上这句话之后,它会先查再调,成功率明显提升。

但这里有个细节:每次操作都查一遍清单,token 消耗会很大。我的优化方案是缓存 + 失效机制。Agent 第一次查完清单后缓存在本地,之后每次操作前只做一个轻量的版本检查(一个很小的接口,只返回版本号)。版本没变就用缓存,变了就重新拉取。

4.4 参数计算与配额设置

配额设置不能拍脑袋,我根据实际使用数据算了一个基准。

假设一个 Agent 平均每天处理 100 个任务,每个任务平均调用 3 个 skill,那么每天总调用量约 300 次。分配到 8 小时工作时间,每小时约 37 次。考虑到峰值可能翻倍,我把大多数 skill 的每小时配额设在 100 次左右。

对于高风险 skill(比如发送邮件、修改数据),配额要严格得多。我的计算方式是:正常使用频率 × 3 倍安全系数。比如发送邮件正常每小时最多 5 次,配额就设 15 次。超过 15 次基本可以判定是异常行为,直接拒绝并记录日志。

{ "send_email": { "max_per_hour": 15, "max_per_day": 50, "cooldown_seconds": 10 } }

cooldown_seconds是两次调用之间的最小间隔,防止 Agent 在短时间内连续调用。这个参数对防止循环特别有效。

5. 常见问题与排查实录

5.1 Agent 不查手册直接调用怎么办

这是最常见的问题。Agent 有时候会“自作聪明”,跳过查询步骤直接调用它记忆中的接口。我的解决方案是在服务端做强制校验。

具体来说,每个 skill 调用请求必须带一个session_token,这个 token 是 Agent 在查询 skill 详情时获得的,有效期 5 分钟。没有 token 或者 token 过期的请求直接拒绝。这样 Agent 就不得不先查再调。

def validate_request(skill_id, session_token): if not session_token: return {"error": "missing_token", "message": "请先获取skill详情"} if not is_token_valid(session_token, skill_id): return {"error": "invalid_token", "message": "token无效或已过期,请重新获取"} return None

这个机制加上之后,跳过查询的情况基本消失了。代价是每个 skill 调用多了一次查询请求,但换来的是可靠性的大幅提升,我觉得很值。

5.2 Skill 定义写得太模糊导致误用

我一开始写的 skill 描述太笼统,比如“搜索信息”这个 skill,Agent 经常在不需要搜索的时候也去搜。后来我把描述改得非常具体:

当用户询问实时信息(如新闻、股价、天气)或你明确知道自己的知识库不包含该信息时使用。当问题涉及常识、数学计算、代码语法等你可以直接回答的内容时,不要使用。

改完之后误用率下降了大概七成。Skill 描述要像给新员工写操作手册一样,具体到“什么情况下用、什么情况下不用”,不能只写“这个工具能做什么”。

5.3 失败重试导致雪崩

有一次我的一个外部 API 挂了,Agent 按照 skill 定义里的重试策略疯狂重试,结果把配额全部耗尽,其他正常的 skill 调用也被限流了。这是个典型的失败放大问题。

我的修复方案是引入熔断机制。当某个 skill 的失败率在 1 分钟内超过 50%,自动熔断 5 分钟,期间所有调用直接返回“服务暂时不可用”。5 分钟后进入半开状态,允许少量请求试探,成功则恢复,失败则继续熔断。

class CircuitBreaker: def __init__(self, failure_threshold=0.5, window_seconds=60, cooldown_seconds=300): self.failure_threshold = failure_threshold self.window_seconds = window_seconds self.cooldown_seconds = cooldown_seconds self.failures = [] self.state = "closed" self.last_failure_time = None

这个机制救了我好几次。现在即使某个外部服务挂了,也不会影响其他 skill 的正常使用。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
Agent 调用不存在的 skill缓存过期或未查询检查 session_token强制先查再调
Skill 被频繁误用描述太模糊查看调用日志细化 when_to_use
调用量突然暴增循环调用或异常查看配额日志加 cooldown 和熔断
失败后卡死重试策略缺失检查 failure_handling补充替代方案
响应变慢外部依赖延迟查看 duration_ms加超时和降级

5.5 几个我踩过的坑

第一个坑是skill 粒度太细。我一开始把每个小操作都拆成一个 skill,结果 Agent 需要调用五六个 skill 才能完成一个简单任务,中间任何一步失败整个任务就挂了。后来我合并了一些高频组合,把粒度控制在“一个 skill 完成一个有意义的操作”这个级别。

第二个坑是示例写得太理想化。我写的 examples 都是完美输入完美输出,但实际使用中 Agent 会遇到各种边界情况。后来我在 examples 里故意加了一些“不完美”的案例,比如输入有错别字、参数缺失等,让 Agent 知道怎么处理。

第三个坑是忘记处理并发。两个 Agent 同时调用同一个 skill,如果这个 skill 有状态(比如计数器),就会出现竞态条件。我的解决方案是尽量让 skill 无状态,有状态的加锁或者用原子操作。

6. 实际效果与后续扩展方向

这套系统上线跑了三个月,我统计了一下数据。Agent 的任务完成率从之前的 62% 提升到了 89%,工具误用率从 23% 降到了 6%,因为工具问题导致的对话中断减少了八成以上。这些数字不是实验室数据,是真实生产环境跑出来的。

当然也不是没有问题。最大的遗留问题是skill 定义的维护成本。每加一个新 skill,我都要认真写 when_to_use、when_not_to_use、failure_handling,这些都需要对业务有深入理解。我试过让 AI 自动生成 skill 定义,效果不太理想,生成的描述太泛,Agent 还是不会用。

后续我打算往两个方向扩展。一个是skill 的组合编排,让 Agent 能定义“先查天气,如果下雨就发提醒邮件”这样的复合技能,而不是每次都要手动串联。另一个是基于使用数据的自动优化,比如某个 skill 的 when_not_to_use 经常被违反,系统自动提示我补充更明确的约束。

最后分享一个小心得:不要试图让 Agent 变聪明,要让环境变清晰。我见过太多人花大量时间调 prompt、换模型,但效果提升有限。真正管用的往往是那些看起来“笨”的办法——把工具定义写清楚、把约束做在环境层面、把失败预案准备好。Agent 不需要是个天才,它只需要一个不会让它犯错的工作环境。

RELATED READING

延伸阅读

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