搞智能体(Agent)这件事,团队里最容易踩的坑,就是把精力全砸在模型选型和提示词调优上,结果一接入真实业务,Agent就变成一个“会聊天但干不了活”的摆设。我去年带队重构某跨平台系统时,把Agent的交互能力、工具调用和业务逻辑彻底拆开,沉淀了一套内部分层方案,项目代号就叫 agent-skills。这篇文章不聊理论,就把我们怎么定义技能、怎么搭技能库、怎么让模型稳定调用工具的完整思路和踩坑记录摊开讲,希望能给正在做Agent落地的朋友省几个星期的摸索时间。
1. 为什么Agent需要一套“技能层”
1.1 先想清楚:Agent到底卡在哪
大部分Agent demo阶段跑得挺顺,一上生产就拉胯,根因往往不在模型聪明不聪明,而在于模型不知道“有哪些工具可以用、什么场景该用哪个、参数该怎么填”。你把一堆函数塞进系统提示词里,模型要么漏看,要么看错,要么调用了但参数格式不对,整体成功率可能只有一半。
我们当时的场景是某跨平台系统的智能助手,背后要接订单查询、物流追踪、商品库存、优惠计算、售后答疑等十几个不同域的服务。如果每个域都在提示词里写一堆接口说明,提示词直接膨胀到爆炸,而且不同接口的参数风格还不统一——有的是订单号,有的是用户ID加上时间范围,模型很容易混。
所以核心矛盾不是“模型能力不够”,而是缺少一个标准化的技能抽象层,把“模型能理解的动作”和“系统能执行的动作”对齐。
1.2 技能层到底封装了什么
我们给“技能”下的定义很简单:一个技能 = 一段清晰的能力描述 + 一份参数约束声明 + 一个可执行的函数入口。
模型不需要知道你的数据库在哪、接口走什么协议、鉴权怎么做,它只需要看到三样东西:
- 技能名字(如
query_order_status) - 自然语言描述:说明这个技能适合处理什么类型的用户请求
- 参数清单:每个参数的名称、类型、是否必填、取值范围
这三样被包装成一个结构化的 JSON 文档,由系统注入到模型的上下文中。用户发来一句话,模型做“意图匹配 → 技能选择 → 参数抽取 → 发起调用”,整个过程不再靠模型自由发挥,而是靠一份明确的协议。
这个设计的核心思路是把决策权和执行权解耦。模型只负责“决定做什么、抽什么参数”,执行层负责“把参数变成真实调用”,两边通过标准接口通信,互相不越界。
1.3 相比裸写函数调用,技能层优势在哪
团队里之前有人觉得:“直接让模型输出一段 JSON 调函数不就行了,搞什么技能层?”确实可以,但只是应付 demo。真实业务里,裸写函数调用有三个致命问题:
第一,接口描述和业务语义混在一起。你告诉模型“GET /api/order?order_id=xxx”,模型记住了,但换个业务域、换套接口风格,它又得重新学。技能层把“查订单”这件事本身的语义固化下来,接口变了只改执行端,模型侧感知不到。
第二,参数校验没有标准。模型输出的参数经常缺胳膊少腿,没有技能层做统一 Schema 校验,你每个接口都得自己写防御代码,工作量爆炸。
第三,无法做权限和审计。技能层天然是一道屏障,哪些角色能用哪些技能、谁调用了什么技能,都能在这层统一管控,这在企业级系统里是硬需求。
2. 技能库设计的核心参数与细节
2.1 技能描述怎么写,模型才听得懂
技能描述是整个技能层里最考功力的部分。写得笼统,模型不知道该在什么时机用;写得太长,上下文塞不下,反而干扰其他技能。我们总结了一条可复用的模板:
技能描述 = 触发场景 + 典型用户表述 + 能力边界 + 输出说明举个例子,我们有个技能是计算优惠后的订单金额。第一版描述是“计算优惠金额”,结果模型经常在用户只问“这个订单多少钱”的时候就调用,传参也乱七八糟。后来改成:
{ "skill_name": "calculate_order_discount", "description": "当用户询问某个订单在应用优惠券、会员折扣、满减活动之后的实际应付金额时使用。典型问法:'这个订单最后多少钱''用了券之后还要付多少'。此技能负责所有优惠叠加计算,不负责退款和发票,也不处理售前咨询。", "parameters": { "order_id": { "type": "string", "required": true, "description": "订单号" } } }改完之后,调用准确率肉眼可见地上来了。核心原因是描述里给了正例(典型问法)和负例(不负责什么),模型对边界的感知清晰了很多。
2.2 参数Schema是命门,一定要往死里抠
参数定义直接影响两个事:模型能不能抽对参数,以及后续校验能不能拦得住脏数据。我们的参数 Schema 直接采用 JSON Schema 的子集,重点约束四个字段:
type:string、number、boolean、object、arrayrequired:哪些参数必须提供enum:枚举值,比如订单类型只允许 online/offlinedescription:每个参数都要解释含义,尤其是易混淆的,比如“下单时间”和“支付时间”
这里有一个我们趟过的坑:参数里不要出现模型无法从用户对话中直接推断的字段。比如某个技能需要内部租户 ID,但用户压根不会说“我的租户是xx”,这种参数必须由系统在调用时自动填充,而不能指望模型猜。否则模型每次都会编一个出来,校验直接炸。
2.3 技能数量膨胀了怎么办
技能少的时候还好,一旦超过 20 个、30 个,把所有技能描述全部注入上下文,Token 开销巨大,模型注意力也会被稀释。我们最终的选择是两层技能路由:先用一个轻量的意图分类模块,把用户请求粗分到某几个候选技能,再把这些候选技能的完整描述注入上下文。
这个方案实测下来,技能选择准确率提升了约 11 个百分点,上下文占用反而降了 40%。代价是多了一次内部调用,但对整体时延影响很小。如果你团队没有单独的意图模型,也可以用 Prompt 让主模型先输出候选技能列表,再走第二步精排。两种方式都可行,关键是别把所有技能一股脑塞给模型。
3. 实操:从零搭一套最小可用的技能调用链路
3.1 步骤一:定义技能清单文件
先建一个技能清单skills.json,所有技能定义统一放这里,方便审计和版本管理。每项技能的结构保持绝对一致,绝不允许执行端各写各的。
[ { "skill_name": "query_stock", "description": "当用户询问某商品是否有货、库存数量时使用。典型问法:'这个还有货吗''库存多少'。仅接受标准商品ID。", "parameters": { "product_id": {"type": "string", "required": true, "description": "标准商品ID"}, "warehouse_code": {"type": "string", "required": false, "description": "仓库编码,不传则默认全部仓库"} } }, { "skill_name": "estimate_delivery", "description": "当用户询问某个订单何时送达、物流进度时使用。典型问法:'什么时候到''物流走到哪了'。", "parameters": { "order_id": {"type": "string", "required": true, "description": "订单号"} } } ]这个文件就是技能的“字典”,模型看到的、执行端加载的、人去 review 的,全是这一份。
3.2 步骤二:写一个极简的技能执行框架
核心是做一个通用的SkillExecutor,负责把模型的技能选择结果翻译成真实函数调用。代码结构大致如下:
# 简化演示,实际项目里会再加配置中心、限流等 class SkillExecutor: def __init__(self): self.registry = {} # skill_name -> callable def register(self, skill_name, func): self.registry[skill_name] = func def execute(self, skill_name, params, context): if skill_name not in self.registry: return {"error": "unknown skill", "skill": skill_name} try: result = self.registry[skill_name](**params, context=context) return {"ok": True, "result": result} except Exception as exc: # 关键:把错误吃进去,返回给模型让它自己修正 return {"ok": False, "error": str(exc), "retryable": True}注意retryable这个字段。执行失败时,系统会把错误信息拼装成一段反馈文本回传给模型,模型根据反馈可以修正参数或者换一个技能,而不是直接给用户报错。这比让模型一次成功的容错率高很多。
3.3 步骤三:跑通一次完整调用链路并记录日志
我们随便模拟一个用户请求“这个手机壳还有货吗,什么时候能送到?”
链路走一遍:
- 意图路由层识别出候选技能:
query_stock、estimate_delivery - 系统把这两个技能的完整定义注入上下文
- 模型判定需要先调
query_stock,参数为{"product_id": "P10023"} - 执行端调用库存服务,返回“华东仓有 58 件”
- 系统把执行结果格式化成模型可读的反馈:
库存查询成功:华东仓 58 件 - 模型继续推理,认为还需要调
estimate_delivery来回答“什么时候送到”,但此时缺少order_id,于是模型回复追问“请提供订单号” - 用户补上订单号,模型再调
estimate_delivery,最终组合成一段自然语言回答
整条链路里,最重要的不是每一步多高效,而是每一步的输入输出都被结构化记录。我们在该环节同步落了一份完整的 JSON 日志:用户原始输入、候选技能排序、最终技能选择、抽取参数、执行结果、模型追问内容,全部有迹可循。
后来排查线上问题,这份日志救了大命。没有它,模型偶尔犯浑时,你根本不知道它在哪一步开始跑偏。
4. 技能编排:让多个技能协作而不是各自为战
4.1 单独技能好做,组合起来才见功力
真实场景里,用户很少只触发一个技能。顺着上一个例子往下走:用户问“手机壳有没有货、什么时候能到”,实际上涉及“库存”和“物流预测”两个技能,而且有先后关系——如果商品根本没货,物流预测就没有意义。
我们刚开始直接让模型“自由发挥”组合技能,结果发现它经常把两个技能并行调用,或者在不该调用的顺序上调。后来引入了一个轻量级的编排声明:在技能清单里增加一个workflow字段,描述技能之间的依赖关系。
{ "workflow_name": "stock_and_delivery", "steps": [ { "skill": "query_stock", "then": "check_stock_result" }, { "condition": "stock_result.available == true", "then": "estimate_delivery" } ] }这里不需要上复杂的流程引擎,一个简单的条件判断就够用。核心是让执行端对执行顺序有把控,而不是完全交给模型临场发挥。模型仍然负责“识别意图、抽取参数”,但执行顺序由编排层兜底,这样即使模型偶然抽风,也不会出现“没货还在预估配送时间”的离谱回答。
4.2 编排层要考虑回退策略
编排不是只能顺序执行。我们压测时遇到过一种情况:主技能调用失败,模型尝试修正参数,但修正两三次还失败,这时不能让 Agent 无限重试。我们在编排层加了两条兜底策略:
第一,单技能最大重试次数默认 2 次,超过后走人工转接或统一话术兜底。
第二,技能降级。比如查库存的主数据源超时,可以降级到备用的缓存数据源;再不行才返回失败。这个机制在促销大促场景特别有用,高并发下主接口大概率会抖。
通过这些策略,技能编排从“看起来能跑”变成了“压力下能扛”。顺便说一句,线上大促那天,这套兜底拦截了约 76% 的技能调用失败,几乎没有让用户感知到异常。
4.3 上下文管理是编排的隐藏关卡
多技能串联之后,模型上下文里会堆入中间过程的执行结果。如果执行结果是一张实时库存大表,几千字直接塞回去,第一是 Token 开销爆炸,第二是模型读完大表可能反而忘了主线。我们的做法是对每次技能执行结果做摘要格式化,只保留模型回答用户所需的精简信息。
比如库存接口返回 20 行明细,摘要成“华东仓有货(58件),华南仓缺货,华北仓有货(12件)”;物流预测返回一大段 JSON,摘要成“预计 3 天内送达”。这样模型拿到的是可以直接用的信息,推理质量会更稳定,上下文长度也一直受控。
5. 常见问题与排查技巧实录
5.1 技能调用一直不准的排查顺序
如果模型频繁不调用技能、或者选错技能,别急着调 Prompt。按这个顺序排查,大概率能找到根源:
先查技能描述是否包含负例边界。很多情况下模型选错,是因为两个技能描述的场景高度重叠,负例能帮它区分。再查参数是否都能从对话中直接提取,如果参数依赖系统侧信息而你没有做自动填充,模型就只能瞎编。最后查候选技能数量,超过 10 个候选,模型的选择准确率会明显下滑,优先考虑加路由层。
我们实际线上问题的分布大概是:约一半是参数问题,三成是描述不清,两成是路由不合理。顺着这个比例去排查,效率会高很多。
5.2 模型自己修正时越修越错怎么办
技能执行失败后,我们会把错误信息回传给模型,让它可以重试。但有些错误信息太长太专业,比如“连接池超时、SQLSTATE 08006”,模型根本看不懂,反而会对着错误信息瞎猜参数,越修越离谱。
解法是把错误信息翻译成模型能理解的语言。执行端抛错时,不要直接回传原始异常,而是丢一个结构化的小标签:参数缺失、参数格式错误、依赖服务不可用、业务校验拒绝。每种标签配一句修复建议,比如参数缺失就提示“请向用户说明需要哪些信息,并等待用户补充”。
这套机制上线后,模型自动修正的成功率从 62% 提到 81%,用户被追问补参数的体验也比直接报错好得多。
5.3 上下文被塞爆的补救手段
多技能组合场景下,就算做了摘要,长时间对话里上下文还是会慢慢累积。我们设置的防线是滑动窗口摘要:对话超过一定轮数后,系统把早期的用户提问和技能调用记录压缩成一段几百字的历史摘要,只保留用户意图和时间线,丢掉详细参数和执行结果。
实测这样处理后,长会话的技能选择准确率只掉了约 3%,Token 开销却省了一半以上。如果你的 Agent 主打一次性问答,可以忽略这一步;但只要涉及多轮对话,上下文管理迟早要做,越早越好。
5.4 常见问题速查表
| 现象 | 排查方向 | 推荐解法 |
|---|---|---|
| 模型完全不调用技能 | 技能描述太抽象,或没有触发场景 | 补典型问法和负例边界 |
| 技能调对了但参数缺 | 参数必须全部能由用户语句推断 | 系统侧字段自动填充,不让模型猜 |
| 选了错误的技能 | 技能间描述相似度过高 | 强化负例区分,或拆分技能粒度 |
| 多技能串联错序 | 执行顺序完全交给模型 | 引入 workflow 编排声明 |
| 上下文越滚越大 | 缺少中间结果摘要 | 执行结果摘要化 + 滑动窗口历史压缩 |
| 失败重试几次仍失败 | 错误信息模型无法理解 | 结构化错误标签 + 修复建议 |
6. 一点实操经验与扩展建议
整个 agent-skills 项目从立项到上线,我个人最大的体会是:技能层的核心不在技术,而在边界感。什么时候让模型决策,什么时候执行层兜底,这套边界的划分决定了系统是稳定可用还是偶尔抽风。初期我们会把所有重担都压在模型身上,结果就是整天救火;后来把边界划清,模型的脆弱性被限制在很小范围内,整体可用性一下就上来了。
如果再给我一次机会,我会把技能评测体系更早做起来。我们现在有一套回归用例集,里面记录了近千条历史用户语句和期望的技能调用链路,每次技能定义改动后自动跑一遍回归,这一步极大降低了改 A 技能弄坏 B 技能的概率。建议你在搭技能层时,同步把这份测评集建起来,越往后收益越大。
后续我们正在做的一件事,是把技能层的方法论平移到更多的业务团队,让每个团队可以用一套配置界面自己维护技能定义,不需要每次改技能都走一次研发发版。等跑顺了,我再把配置化运营的经验整理出来分享。