AI代理技能系统:从工具函数到可验证的行为契约
代理工程做到一定阶段都会碰到同一个坎单次模型调用在 demo 里什么都能干可一旦对业务结果负责模型本身的输出方差就成了最大的隐患。真正的可靠性往往不来自模型而来自它周围那层被称为技能skills的工程框架。agent-skills 这个仓库名从一个内部代号慢慢变成了我们团队的核心理念把模型从“什么都会一点”变成“有固定套路、可验证、可升级”的专家组合体。这篇文章我会把技能的几种定义方式、必须画清楚的边界、与记忆系统的关系、生产落地的编排方案以及实测下的一些基线数据串起来讲给还没搭过技能系统的同学一份可以直接照着思考的参考。1. 先定个基调代理的“技能”和传统“工具函数”到底差在哪1.1 工具函数只是“能力”技能是“可验收的行为契约”如果你接触过 Function Calling第一反应肯定是技能不就是把一段提示词和API调用包一下和工具函数有什么本质区别我当初也这么想直到第一次被线上问题打脸。工具函数解决的是“模型能不能做”的问题比如能不能查数据库、能不能发邮件。能力有了但模型在什么条件下查库、查询结果怎么进上下文、失败之后要不要换一种方式这些仍然是模型临场发挥。技能系统的核心恰恰是把这些临场发挥固定下来它是一段可重复执行的行为流程而不是一个孤立函数。打一个比方工具函数是给代理一把扳手技能则是培养代理“拧这颗螺丝时要先检查螺纹、再对准角度、用力矩扳手分三次拧紧”的肌肉记忆。扳手可能被拿去砸核桃但技能必须保证它在正确场景下做正确动作而且做错时你能知道错在哪一步。我一般用三个指标判断一个函数该不该升级成技能是否被多个场景复用是否涉及多步操作、中间状态或失败重试是否对业务结果有可量化的要求比如正确率、延迟、成本。三者只要命中任意两项就应该走技能化改造。1.2 技能给代理带来的三个收益稳定性、可测试性、可观测性技能带来的收益我认为可以压成三个关键词。第一是稳定性。模型每次调用输出方差很大但技能通过约束提示词模板、校验输出结构、规定异常分支把方差收敛到可控范围。稳定性不是靠提示词写得天花乱坠而是靠“所有出口都被校验过”的设计。第二是可测试性。单体模型调用只能做端到端测试出了问题很难定位。而技能是一个有明确输入、输出和副作用声明的单元你可以像测普通函数一样测它可以构造 mock 外部服务做回归提示词改完能立即看出对结构化输出的影响。第三是可观测性。每个技能执行时有明确的开始、结束、成功率、耗时、token 消耗。这些指标拼起来你才可能说出“周五下午代理失败率涨了是因为天气技能依赖的第三方接口超时”而不是对着黑盒瞎猜。这三个收益不是白来的代价是你要做额外的建模和工程工作技能系统本身会引入一层调度开销。如果你的代理只是个简单 demo不涉及多条业务线和真实用户我反而不建议一上来就搭完整技能体系。过度设计比没有设计更容易拖垮项目这个取舍要自己掂量。2. 技能的定义方式提示模板、工具与心智管道组成的“三明治”2.1 技能注册表所有技能要有统一的描述语言技能化的第一个实操决定是建立一个技能注册表。无论你底层用 LangChain、自研框架还是裸 SDK注册表都应该是唯一可信的技能来源。每个技能在注册表里至少包含五个字段字段含义示例name全局唯一技能名weather_querydescription给模型看的自然语言说明包含使用场景和边界查询指定城市当日天气仅支持主要城市input_schemaJSON Schema 格式的输入校验城市编码、日期output_schema结构化输出定义天气代码、温度区间handler实际执行逻辑的引用指向内部函数或外部服务注册表的价值不只是文档它承担着运行时路由模型要调用技能时系统根据 description 和 input_schema 做意图匹配与参数校验。我踩过的一个典型坑是 description 写得过于随意结果模型经常把“查询天气”误配到“查询节假日”上。后来把所有 description 统一改成“场景 功能 边界 典型问法”四段式误配率明显降了下来。2.2 提示模板技能里的“不变参数”与“可变参数”技能内部最直观的部分是提示模板。以“总结对话并生成待办”这个技能为例SKILL_PROMPT 你是对话待办提取器。 输入以下是一段最近对话记录过滤掉寒暄与无关信息只保留动作性内容。 输出JSON 数组每项含 title、owner、due_at、priority。 对话记录 {{conversation_text}} 输出约束 - 未明确指定 deadline 的项due_at 必须为 null不得自行猜测。 - owner 无法判断时返回 unassigned。 - 只返回 JSON不要额外解释。 这里有几个关键点。第一模板里的可变参数如 conversation_text一定要用占位符显式标注并在外部完成插值不要在模型上下文里现场拼一段不可控文字。第二输出约束写在模板末尾、靠近模型生成位置对稳定格式更有效。第三提示模板必须带版本号因为改提示词是最容易引发回归的操作灰度章节我会详细讲。2.3 工具封装与“心智管道”技能不等于一个函数调用如果一个技能只有提示词加一个函数那它还称不上技能顶多是“带提示的工具”。真正的技能通常包含一条心智管道也就是把多个操作按固定顺序编排预处理归一化输入补齐缺失参数检索从记忆库或知识库取相关上下文推理调用模型生成中间结果执行调用一个或多个外部工具校验对结果做 schema 校验和业务规则校验回写把关键结果写回记忆或日志。举个例子“帮用户查航班并提醒去机场时间”这个技能内部其实串了航班查询、城市距离计算、交通耗时估计三个子模块。如果不包成一个技能模型就要自己决定怎么拼接这会带来灾难性的连续失败——某个中间工具返回格式稍有变化整个推理就崩了。包成技能之后中间过程对模型不可见只有最终结果进入模型上下文整体容错性会高一个量级。这也是为什么我一直建议技能要把“决策点”尽量收敛模型只需要在技能边界做决策技能内部尽量做成确定性流程。确定性越多代理越可靠。3. 设计技能时的三个边界输入输出、失败边界、状态边界技能系统里百分之八十的设计问题都出在边界没画清楚。边界画得好技能就是乐高积木画不好技能就是一团乱麻。3.1 输入输出边界拒绝自由文本进技能第一个教训是不要让技能直接接受模型抛过来的自由文本参数。最初我们的“查询天气”技能 input_schema 里城市字段是自由文本模型可能传“北京”“北京市”“Beijing”“今天北京天气咋样”。同一个语义五种写法后续匹配和缓存全部紊乱。后来我强制所有文本参数进技能前先过一个轻量 NER 加归一化层把实体映射到标准编码比如城市映射到行政区划代码。代价是多一次模型或词典调用收益是下游逻辑全部清爽了。输出边界同理。技能返回给模型的内容必须是紧凑、结构化的结果描述而不是把原始 API 返回的一大坨 JSON 原样丢回去。很多接口返回里有大量无用字段轻则浪费 token重则干扰模型推理。我习惯在技能内部做一次“投影”只保留模型继续推理真正需要的字段再附上必要的时间戳和置信度。3.2 失败边界技能必须能告诉代理“为什么没做成”代理系统最怕的不是失败而是失败得不明不白。模型如果只知道工具返回了个错误它就会开始瞎猜然后编造一个看似合理的替代结果这是最危险的。因此我对技能有个硬性要求任何失败都必须归类并且给出可执行的错误语义。至少分四类错误类型含义模型应该拿到的信息INPUT_INVALID输入参数非法具体哪个字段不合规合规范围是什么UPSTREAM_ERROR依赖的外部服务挂了服务名、超时、是否可重试POLICY_REJECT业务规则不允许触发了哪条规则改什么输入才能通过SKILL_INTERNAL技能内部逻辑异常大概哪个环节失败能否降级比如支付技能遇到余额不足时返回 POLICY_REJECT 并附带“可用支付方式列表”模型就能据此引导用户换一种方式而不是反复调用同一技能。这个决策链条看起来很小但在真实产品里决定了用户会觉得产品“挺聪明”还是觉得你在调戏他。3.3 状态边界技能不得偷偷修改全局状态第三个边界是状态。技能在承担多步任务时经常需要读写状态但我的建议很明确技能的写操作必须声明且收口不允许顺手改全局会话状态。简单来说每个技能内部只能维护自己的局部状态跨技能共享的信息必须显式通过返回值或专用记忆接口传递。如果让技能直接改全局状态你很快就会看到两个技能之间的隐性耦合——比如订票技能改了日历技能依赖的时区设置后面所有行为就不可预测了。我实践过的一个土办法是给技能上下文对象设置只读视图和写时复制快照。技能读到的永远是进入时刻的上下文写完落库时统一提交并记录 diff。这么设计之后跨技能排查问题的效率提升非常明显因为每个技能看到了什么、改了什么都一目了然。4. 技能和记忆是两码事理不清这层关系代理永远学不聪明4.1 技能读取记忆的三种方式这里想纠正一个常见误区技能列表本身不是记忆。技能是“动作”记忆是“事实”。如果一个代理只有技能而没有记忆系统它就像精通各种拳法但没有战术意识的格斗新手每次都从零理解局面。技能读取记忆有三种典型方式一次性注入把和当前任务强相关的记忆片段直接拼进技能提示词适合单轮技能动态检索注入先从记忆库按相似度召回 Top-K 片段再注入适合知识密集型技能工具式读取技能内通过专门 API 查询记忆适合需要精确数据的技能。我在实践中发现大多数人一上来就喜欢动态检索注入因为听起来高级。但动态检索会引入噪声检回来的片段一旦不准对模型的误导比没有还严重。我的建议是先做一次性注入把会话级上下文整理清楚跑通了再按需引入检索。大多数场景下一层精心整理的会话摘要比什么都管用。检索真正要解决的问题是跨会话的长期事实比如“用户上周说过自己不吃辣”而不是“用户刚才问了一句天气”。4.2 技能之间的信息交接规范技能之间怎么传信息决定了一个多技能代理看起来是个整体还是几个散兵各自为战。我坚持的规范是技能之间不直接通信要传消息必须经过一个运行上下文对象。代理每次任务开始时会建立一个运行时上下文包含用户画像摘要、会话摘要、当前任务目标、已执行技能列表。每个技能运行时读它结束时把输出写回它。这样既避免状态边界问题也让链路天然可追溯每步之后上下文变成什么样都能从日志重放出来。这个设计思路简单但建模要求很高。你需要为“任务”定义一个清晰抽象而不是让每个技能自己拼凑信息。任务级上下文是技能间的共享白板谁都能看但写入必须按规范来。初期这个抽象可以很简陋一个字典都行但字段命名和写入时机一定要尽早统一不然后期重构成本很高。4.3 技能的回写策略会改变记忆才叫学到东西既然花了力气把技能做成可观测、可测试的单元那它理应能为记忆系统提供素材。技能回写记忆主要有三种类型事实回写比如用户属性、偏好、明确声明过的信息结果回写比如上次任务结果摘要、执行成功或失败记录过程回写比如“用户对某一种表达方式有明显负面反馈”。回写策略必须克制。我见过一个代理每次技能执行完都把全过程写进长期记忆结果记忆库迅速膨胀检索时噪声爆表。我的习惯是给回写设白名单哪些技能、哪些字段级别才能落长期记忆其他只落短期会话上下文或干脆不落。说到技能自省有些团队在做模型复盘的尝试。自省不是不能做但一定要有明确触发条件比如失败之后或用户明确不满意时并且限定复盘范围否则每次技能完成都拉着模型自吹自擂一遍token 成本高不说还可能把本来正确的输出越改越错。5. 技能编排的生产落地顺序流、DAG 与规划器的取舍当技能数量超过二十个你就必须面对“谁决定技能调用顺序”的问题。这个问题的答案直接决定项目成败。5.1 全交给 LLM 规划器天花板高但地板也很低最理想的愿景是模型看着用户需求自己规划调用哪几个技能、按什么顺序。这种动态规划的天花板确实高尤其适合开放性问题。但落地时要注意LLM 规划器的可靠性上限受制于意图识别和长链路错误累积。用户说一句“帮我安排明天下午的会议并预订一个离公司近的餐厅”模型要正确触发会议创建、会议地址解析、餐厅推荐、日历冲突检查等多个技能中间任何一步的实体错误都会像滚雪球一样放大。我实测下来超过三步的动态规划端到端成功率大概在七成到八成五之间而且很难单靠提示词提升必须配合约束手段。5.2 关键路径要确定性把能固定的编排固化成 DAG因此我强烈建议业务关键链路不要完全交给模型即兴发挥而是把主干流程固化成确定性 DAG只在分支点给模型小范围选择权。具体做法是定义技能流的 DSL比如下面这样workflow: id: meeting_plus_restaurant steps: - id: s1 skill: calendar_query next: s2 - id: s2 skill: address_resolver next: s3 - id: s3 skill: restaurant_recommend next: s4 - id: s4 skill: calendar_create next: END branches: - from: s2 condition: 用户未提供理想位置 target: location_picker这个流程里模型不是没有决策而是决策被收敛到条件分支上。比如地址缺失时要不要触发 location_picker这种决策交给模型没问题但“解析完地址必须推荐餐厅”这种主干顺序就不该让模型自由发挥。我把这种设计叫“宽入口、窄决策”任务入口尽量理解用户意图内部流程尽量确定性最后留少量小范围的选择点。它的可靠性远高于全链路 LLM 自主规划。5.3 运行时追踪是技能系统的仪表盘没有追踪技能系统就是个黑盒。我做技能调度的日志最少记录这些维度技能 ID、版本、输入哈希开始和结束时间、耗时、token 消耗每一步子操作的耗时分布最终状态和错误码进入时的任务摘要和写回的关键字段。有了这些数据你才能做真正的调优。比如我发现某个技能经常在检索阶段花掉四成耗时就去优化索引发现另一个技能错误集中在 UPSTREAM_ERROR就和外部服务方谈加超时、加缓存。没有观测数据这些优化全是拍脑袋。我还强烈建议对技能执行链路打上 trace id。一个用户请求可能跨五个技能如果各技能日志彼此独立排查跨技能问题时会非常痛苦。trace id 贯穿全链路之后定位时间可以缩短一个数量级。5.4 灰度与回滚改提示词比改代码更容易翻车提示词这东西改一个词都可能改变输出概率分布。我见过不止一次某团队改了一个技能的 system prompt线上成功率瞬间掉了十个点还没反应就已经影响一批用户。所以技能系统必须有版本控制与灰度能力。我的做法是每个技能的提示词、工具封装、流程编排都作为独立可发布的版本灰度按 1%、10%、50%、100% 逐步放量每一步对比老版本的成功率、延迟、token 消耗、用户反馈率任何指标异常立即回滚到稳定版本。这个机制看起来重但技能数量上到几十个之后它能救你无数次。前阵子我们灰度新一版订单查询技能总体成功率从 98.2% 跌到 96.0%单看不算太糟但按场景细分后发现“查询历史订单”场景的成功率崩到了 78%。因为灰度只放量了 5%影响面小很快定位到是提示词里对“历史订单的时间范围描述”改出了歧义回滚后恢复正常。这个案例很能说明灰度加细分监控的价值。6. 技能安全与权限被低估的一层防线很多团队做代理时把精力全放在“能不能完成任务”上却忘了问“这个技能是否被越权使用、是否泄漏了不该泄漏的信息”。技能化其实给权限控制提供了一个天然的抓手。6.1 技能的最小权限原则每个技能都应该只获得完成自身任务所需的最小权限。例如一个查询订单技能从设计上就不应该能访问用户支付凭证一个写日历技能不应该有删除其他用户日历的权限。实现上我给技能上下文对象挂了一个权限令牌技能在执行外部调用前必须校验令牌范围。令牌不由模型生成而是由技能注册表在匹配技能时按预设策略签发。模型再怎么提示词注入也无法绕过这层校验因为权限判断发生在技能内部代码里。这里有一个容易被忽略的细节技能描述文本本身也可能被模型曲解为更高权限。比如模型可能把“查询”说成“导出”来试图触发不同技能所以技能路由时除了匹配名称还要校验意图与技能权限级别是否匹配。我们的方案是对每个技能记录一个 allowed_intents 列表模型选择的技能如果不在允许意图范围内直接拒绝。6.2 技能调用的审计与熔断技能系统还应该天然生成审计日志。谁触发了什么技能、传了什么参数、返回了什么结果、涉及哪些敏感字段这些都要留痕。审计日志不需要每次排查都看但出了安全事故时它是唯一能说清楚“发生了什么”的证据。熔断机制则用来防止技能在异常状态下反复冲击下游。比如某个外部接口连续失败了 5 次自动熔断 30 秒期间直接返回降级结果而不是继续烧 token 重试。这个策略对代理特别有效因为模型默认是不会主动止损的它只会不断尝试直到把服务拖垮。敏感操作类技能比如删除、退款、对外发送消息我建议做成双确认结构第一阶段返回确认摘要第二阶段要求模型以明确指令确认后再真正执行。虽然多一次交互但对高风险动作是值得的。7. 我实测过的一些基线数据和颗粒化准则这一节分享一组我自己跑出来的数据和设计准则。不同团队场景不同但作为起步基线是够用的。7.1 技能调度的真实开销技能系统是有成本的。我的实测数据模拟项目单技能平均步数四到六步大致是这样环节耗时占比说明模型推理70%-80%技能的核心计算工具外部调用15%-25%依赖网络 IO是延迟波动主因技能调度与校验2%-5%注册表路由、参数校验、日志写入记忆读写3%-7%检索和回写调度与校验本身的开销很小但如果每个技能都做检索记忆环节就会明显膨大。所以我会在技能定义里显式声明是否需要检索无检索需求的技能不默认加检索。另一个值得注意的现象是输入压缩的效果。我们把技能返回给模型的长文本投影后平均 token 消耗下降了大约三成而任务成功率并没有下降。这印证了“给模型太多无关信息会干扰推理”的直觉。当然也别压缩太狠丢掉关键上下文会让正确率掉下来这个平衡点必须用实验调。7.2 技能颗粒化的两个准则技能粒度过大会增加复用难度和失败面太小又会让调度与追踪成本压过收益。两个准则供参考准则一以业务结果和失败模式为依据。一段流程如果有一种明确的主失败模式就值得独立成技能如果多种失败模式混杂且互相影响则应继续拆细。准则二以复用频率为依据。多个场景都会用的原子操作比如地址标准化、实体归一化做成基础技能只在特定流程里出现的组合做成流程技能。以酒店预订为例我们拆成搜索酒店、价格比较、预订提交、取消预订四个技能。搜索、比较、提交的组合包另外封装了一个一键订房的高级技能内部依然顺序调用前三者。这个组合牺牲了一点灵活性但换来了更稳的用户体验适合高频场景。7.3 小心输入摘要折损输出质量还有一个提醒很多人为了省 token会把用户输入先做一轮摘要再传给技能。摘要确实省 token但会丢细节尤其是在数字、时间、否定词上。模型摘要漏掉一个“不”字整个技能输出方向就反了。我踩过的坑是某个技能做用户意见分类为了省 token 把用户留言压缩成一句话摘要结果把“我不喜欢这个功能”压成“用户提及此功能”分类结果完全变了。从那以后涉及否定语义、数值、时间实体的内容一律不做摘要至少要保留原文关键片段。调度上的输入摘要要慎之又慎省 token 的优先级永远低于正确性。8. 从技能库到能力地图往前一步还能做什么8.1 能力地图不再让模型盲选技能多到一定规模模型光靠注册表的 description 去选技能会产生严重的选择疲劳。我现在在技能库之上建了一层能力地图把技能按用户意图聚类映射。例如用户说“帮我订餐厅”不会直接触发 restaurant_recommend而是先进入 dining_intent 这个能力节点由它调度位置偏好、口味偏好、时间约束、预订执行四个技能。这一层带来的好处是技能不再是孤岛而是面对某类用户需求时的标准化组合。它同时成为产品能力的可视化地图运营和产品可以直接看到代理目前能处理什么、不能处理什么比甩一堆代码直观得多。能力地图还有一个隐性的价值它为技能迭代提供了方向。某个能力节点如果经常失败说明这个能力还没被真正打磨好应该优先补齐数据或重构流程而不是继续增加新技能来掩盖旧技能的缺口。8.2 技能的新建成本要持续下降最后说个方向性观点。一个技能系统是否健康不看它现在有多少技能而是看“上线一个新技能”的成本。如果一个新技能从需求到上线要三周那这套系统一定长得不健康。我见过比较健康的团队一个新技能从编写、测试到灰度上线周期压到一到两天核心靠的是完善的脚手架和高度规范化的注册表。技能库不是用来堆数量的它是用来持续积累可复用经验的。随着业务理解加深技能库会从几十个函数长成一组带边界、带契约、带观测的业务能力资产模型越来越像一个跑在这些资产上的执行器。到那时代理优秀与否已经不取决于模型能不能灵光一现而取决于你给它底座上的技能有多扎实。最后分享一条个人经验不要急着把什么都做成技能先挑那个让你每天夜里睡不好的业务环节把它做成第一个技能跑出收益和标准流程再逐步扩充。从一个技能开始远比纸上规划二十个技能然后放弃更接近成功。技能系统最大的价值不是技术架构本身而是它逼着你把不可控的模型行为变成可依赖的业务能力。