Agent技能体系实战:如何将大模型应用从玩具Demo变为可靠工程
2026/9/23 7:28:32 网站建设 项目流程

这套“agent-skills”的目录我反复看了很多遍,最后确认了一件事:市面上讲Agent的文章绝大多数在讲怎么调Prompt、怎么选模型、怎么搭RAG,真正把Agent当作一套“有岗位分工、有技能边界、有治理规则”的工程体系来聊的很少。前阵子我带团队做内部客服助手,一开始直接拿大模型裸跑业务,结果被一堆莫名其妙的问题干懵了。后来把整个系统重构为以技能(Skill)为核心的组织方式,模型还是那个模型,效果却完全不一样。这篇就把我在这个过程中的思考、踩坑和最终沉淀下来的方法完整拆开,给正在做Agent应用、想摆脱“玩具级Demo”状态的同行一些可直接落地的思路。

1. 先想清楚一个问题:Agent为什么需要“技能”这个概念

1.1 裸用大模型跑业务,失败是必然的

我先讲几段真实经历。第一次做客服助手的时候,我们的方案很“朴素”:把公司知识库塞进Prompt,再加上一堆“你要遵守以下规则”的指令,然后让大模型直接回答客户问题。Demo阶段确实惊艳,随便问什么都能答得头头是道。但一放到真实流量上,问题全冒出来了。

三类问题最有代表性。

第一类是“答非所问”。用户问“我的订单为什么还没发货”,模型能洋洋洒洒写五百字,从物流行业聊到天气影响,最后也没有调取任何订单接口。它不是不知道要调接口,而是Prompt里虽然写了“你可以调用工具查询订单”,但没人告诉它“在什么条件下调用、调完拿返回值做什么”,它的能力从来就没有被结构化地组织过。

第二类是“做到一半就断”。用户说“帮我取消订单并退款”,理想流程是:先查询订单状态,再判断是否允许取消,然后执行取消,最后发起退款。模型往往做到第一步或者第二步就停了,然后煞有介事地告诉用户“已经处理完毕”。这就是典型的没有流程编排能力,模型自由发挥的边界太大了。

第三类是“格式彻底崩坏”。有一段时间我们要求模型输出Json,它确实输出了Json,但字段有时叫“order_id”,有时叫“oid”,有时干脆在Json外面包一层Markdown代码块。下游解析程序直接炸掉。

这些问题的根因不是模型不够聪明,而是我们天真地把Agent当成一个“超级大脑”来用,什么逻辑都靠它现场推理、现场发挥。人不是这么干活的——一个有经验的老员工,手里会有一堆已经内化的SOP、工具、模板,遇到什么场景就翻出对应的方法来执行,而不是从零开始想“我该怎么处理这个问题”。

技能(Skill)就是这层“内化的SOP+工具+模板”。它把那些可以提前确定的行为模式从模型身上剥离出来,固化成模块,让模型只在真正需要决策的地方做决策。

1.2 技能化的本质:把“熵”收进边界里

从工程的角度看,大模型的输出天然带有不确定性,这是它强大的来源,也是它难以直接进生产系统的死穴。技能化做的事情,本质上不是抹掉这种不确定性,而是把它压缩到尽量小的范围内。

我把一个Agent系统的决策分成两类:一类是“稳定决策”,一类是“随机决策”。

稳定决策指的是那些可以被规则、模板、工具封装的东西。比如订单状态的查询逻辑、数据清洗的固定处理流程、发送邮件的调用参数组装。这些事一旦确定下来,就永远不应该变,也不能让模型每次发挥。随机决策则是指那些真正需要理解语义、判断用户意图、生成自然语言的环节,比如“用户这句话是不是在表达不满”“这个售后问题应该归到哪个类别”。

技能体系的全部意义,就是把稳定决策变成一个个黑盒技能,模型只负责在技能之间做随机决策。这样即使模型某次“发挥失常”,它最多是选错了技能,而不是在执行过程中把流程搞得一塌糊涂。错误范围被收敛了,系统才有可能被调试、被度量、被治理。

围绕这个目标,我们后来的架构从“一个巨大的Prompt”变成了三层结构:

  • 能力层:实际执行动作的代码、API调用、脚本,也就是技能的“身体”;
  • 描述层:用自然语言把技能的能力范围、触发条件、参数要求写清楚,这是给模型看的“说明书”;
  • 编排层:决定给定一个任务,按什么顺序调用哪些技能,以及单个技能失败后怎么处理。

agent-skills这个词如果要做成一套真正可复用的方法论,核心就是把这三层分别做好,并且让它们之间的接口足够清晰。我接下来按这个顺序逐一展开。

2. 技能的接口设计:给模型看的说明书,和给人看的文档完全是两回事

2.1 一份技能元数据该长什么样

先上一个我们实际在用的技能定义示例,这是整套体系里最基础也最容易被做砸的一环。技能注册表里每个技能就是这样一个描述块:

{ "name": "query_order_status", "description": "当用户询问订单物流、发货时间、订单状态、签收情况时,使用本技能查询订单实时状态。不要用本技能处理退货退款,退货退款请走 return_order 技能。若查询返回的状态为 canceled,请直接告知用户订单已取消并询问是否需要重新下单。", "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "用户提供的订单号,格式通常为 10 位数字或字母组合。若用户未提供,请先向用户索要订单号。" }, "user_id": { "type": "string", "description": "用户唯一标识,用于权限校验,从会话上下文中自动获取,无需询问用户。", "required": true } }, "required": ["order_id", "user_id"] }, "output_schema": { "type": "object", "properties": { "order_status": { "type": "string", "enum": ["pending", "processing", "shipped", "delivered", "canceled"] }, "estimated_delivery": { "type": "string", "format": "date" }, "carrier": { "type": "string" }, "tracking_number": { "type": "string" } } } }

很多人看到这个描述块的第一反应是“这不就是个简化版的OpenAPI规范吗”。对,形式上很像,但有一个关键差别:OpenAPI是给开发者看的技术契约,而这份元数据的首要读者是模型。模型不会像人一样去“读文档”,它是通过描述里的语义关联来判断“当前这句话跟哪个技能扯得上关系”。

所以description字段的写法跟写API文档的逻辑完全不同。人看的文档会写“该接口用于查询订单状态,参数order_id为订单编号”,甚至默认调用方自己懂业务。给模型看的描述必须回答三个问题:什么时候用、什么时候不用、用了之后返回值怎么处理。

我在描述里写了“不要用本技能处理退货退款,请走return_order技能”,这就是在主动给模型画边界。实测下来,这种“负向提示”对准确率的影响非常明显。没有这句之前,大约15%的退货类问题会误调到查询订单状态,加了这个边界说明之后,这个比例降到了3%左右。模型本质上是在做语义匹配,你给它说得越清楚的边界,它误判的空间就越小。

2.2 参数设计要压缩自由度

接着说参数设计。很多人在设计技能输入参数时,习惯照着内部系统接口的字段原样搬。但模型决定参数的过程本身就是一个随机过程,参数越开放,模型出错的机会越多。

我们在input_schema里尽量做三件事。

第一,能用枚举约束的就用枚举。直接把受限的取值列表写进schema,模型在大多数情况下会自动从枚举里选,而不是自己编一个。比如订单状态就只有五种值,告诉模型可能的状态值,它就不会花式发挥。

第二,把能从上下文里拿的参数标记为自动注入,不要让模型去问用户。user_id就是这么处理的,会话一开始就通过登录态拿好,描述里直接告诉模型“无需询问用户”。否则模型可能在处理到一半时突然停下来,问一句“请提供您的用户ID”,把整个交互体验搞得极其诡异。

第三,为每个参数写清楚“若用户未提供,应该怎么做”。order_id这个参数我们写了“若用户未提供,请先向用户索要”,这意味着模型会把“索要订单号”当成一个合法的中间行为,而不是卡在那里不知道下一步。

这些细节看起来琐碎,但本质上是在降低模型做决策的难度。模型不是神,给它越清晰的路标,它走偏的成本就越高。

关于参数的类型,我们统一用string和enum为主,尽量不直接用自由格式的object嵌套。模型生成嵌套Json的能力虽然不错,但只要一个层级错了,解析端就会很狼狈。宁可技能内部多做一步解析,也不要把复杂的嵌套结构交给模型来编。

2.3 输出设计决定了你的Agent是“能用”还是“好用”

输出这块踩的坑最多。最初我们的技能返回原始Json,让模型自己挑选需要的信息并组织语言。结果就是模型经常“自由发挥”:它会把Json里的字段名直接念给用户,或者漏掉关键信息,甚至凭空编一个不存在的状态。

后来我们每个技能改了输出结构,分为两段:一段是结构化结果,供编排逻辑和后续技能使用;一段是自然语言摘要,供模型直接转述给用户。

{ "result": { "order_status": "shipped", "estimated_delivery": "2025-03-20", "carrier": "顺丰速运", "tracking_number": "SF1234567890" }, "summary": "订单已发货,承运方为顺丰速运,运单号 SF1234567890,预计 3 月 20 日送达。" }

summary字段是我们人为拼接的,不让模型自己总结。为什么?因为给用户的回复句子一旦交给模型自由发挥,它就可能在中间插一句“需要我为您查询其他订单吗”,或者把“预计送达3月20日”说成“预计3月20号左右”,虽然无伤大雅,但你永远不知道它下一句会冒出来什么。

在To B和严肃业务场景里,给用户的最终回复宁可生硬一点,也要保证每次说的关键信息一致。模型可以做的事情是在summary基础上做轻度润色,比如调整语气、补充礼貌用语,但不能改动事实字段。也就是“事实由技能锁定,风格由模型发挥”。

还有一点要注意:错误信息也要结构化。技能执行失败时,返回的不是一个简单字符串,而是错误码和错误上下文。这个在后面讲编排的时候会具体展开。

3. 路由与编排:Agent怎么知道什么时候用哪个技能

3.1 从“全部塞进Prompt”到“按需加载”

技能数量少的时候,把所有的技能描述全都塞进系统Prompt里,让模型自己挑,问题不大。但一旦技能超过二十个,这条路就走不通了。

一方面Token消耗会变得很夸张,几十个技能的完整描述加起来动辄上万Token,每次请求都带着这些内容,成本直接起飞。另一方面,技能描述之间会产生互相干扰。模型看到一堆功能相近的描述,选择准确率反而下降。这跟人在面对一份两百个选项的菜单时会犹豫一样,选项越多,决策质量越差。

所以我们做了技能路由。核心逻辑很简单:在把Prompt发给模型之前,先根据用户输入,从技能库里召回一小批最相关的技能描述,只把这些描述注入系统Prompt。这个环节相当于给模型划了一个“今天的重点工作范围”。

我们用的是一个非常朴素的实现,但没有花哨模型也能跑得很好。核心思路是给每个技能维护一组“触发特征”,再跟用户输入做相似度匹配:

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity import numpy as np class SkillRouter: def __init__(self, skill_registry): self.skill_registry = skill_registry self.trigger_texts = [] self.skill_names = [] for skill in skill_registry: self.skill_names.append(skill["name"]) # 触发特征 = 技能描述 + 预先提取的典型问法 + 同义词集合 trigger_text = f"{skill['description']} {' '.join(skill['trigger_phrases'])}" self.trigger_texts.append(trigger_text) self.vectorizer = TfidfVectorizer( ngram_range=(1, 2), stop_words="zh", max_features=5000 ) self.tfidf_matrix = self.vectorizer.fit_transform(self.trigger_texts) def route(self, user_input, top_k=5): input_vec = self.vectorizer.transform([user_input]) scores = cosine_similarity(input_vec, self.tfidf_matrix)[0] top_indices = np.argsort(scores)[::-1][:top_k] candidates = [] for idx in top_indices: if scores[idx] > 0.25: # 低于阈值就不召回,留给兜底流程 candidates.append({ "skill": self.skill_registry[idx], "score": float(scores[idx]) }) return candidates

初学者可能会问:为什么不用大模型直接做路由?我们的经验是:在技能数量几百以内这个量级,用TF-IDF加余弦相似度的字典方法,准确率足够,延迟接近零,成本可以忽略。大模型路由虽然语义理解更强,但一次路由就要消耗几千Token,而且它自己也有出错的可能。路由环节的错误会传导到后面的所有决策,所以在这个点上我们用确定性最强的方式。

召回之后,这些候选技能描述会被拼进系统Prompt里。注意,不是所有召回的技能都要展示,只有得分超过阈值的才展示。我们体会下来top_k控制在4到6个比较合适——太少可能遗漏正确技能,太多又会让模型重新陷入“选择困难”。

3.2 编排:像写主流程一样设计技能的调用顺序

路由解决的是“第一步调用谁”,编排解决的是“接下来调用谁”。这是两个完全不同的问题。

我们早期把编排也交给模型自由发挥,结果就是流程不可控。后来想明白一个道理:绝大多数业务流是固定的,没有理由让模型每次重新发明一遍。比如退货流程,从发起申请到审核通过再到退款,这条链路的每一步都是确定的,唯一需要模型判断的是起点和几个条件分支。这种情况下,编排逻辑应该写死在代码里,模型只负责提供判断依据。

我们采用了一种类似“状态机+决策节点”的轻量编排模型。每一步是一个节点,节点可以是技能调用,也可以是条件判断。模型在条件判断节点上做决定,但流程走向完全由代码控制。

举例来说,一个简单的退款流程编排:

class RefundWorkflow: def __init__(self, skill_runner): self.runner = skill_runner def execute(self, session): # 节点1: 查询订单状态 order = self.runner.run_skill(session, "query_order_status", { "order_id": session.context["order_id"] }) if order["result"]["order_status"] != "delivered": return {"type": "reject", "reason": "订单未完成配送,无法发起退款"} # 节点2: 让模型判断是否需要人工审核 need_review = self.runner.ask_llm( session, prompt="根据订单信息和用户描述,判断该退款申请是否需要人工介入。" "只有当金额超过500元或商品已使用时才需要人工介入。", output_format="boolean" ) if need_review: return {"type": "manual_review", "reason": "系统判定需要人工审核"} # 节点3: 执行退款 refund_result = self.runner.run_skill(session, "execute_refund", { "order_id": session.context["order_id"], "amount": order["result"]["paid_amount"] }) return {"type": "success", "data": refund_result}

这里最大的设计决策是:**能让代码判断的,就不让模型判断;只有真正需要语义理解的地方,才把模型插进去。**需要模型做判断的地方(比如“是否需要人工介入”),我们用明确的Prompt限定了判断依据,模型只需要输出true或false,不需要解释,也不需要自己想往左拐还是往右拐。

为什么跟模型交互时输出格式越简单越好?因为自由文本输出会给下游流程带来不可预料的解析问题。布尔值判断加上枚举输出,是我们在生产环境里踩过很多坑之后总结出来的最优解。

3.3 失败处理:给Agent一条“台阶”而不是一个“悬崖”

技能调用一定会失败。网络超时、外部接口报错、参数不合法、权限不足,每种失败都应该有对应的处理策略,而很多人在设计Agent时根本没有这个环节。结果就是技能一报错,Agent整个就卡在那里,或者开始胡说八道。

我们把技能失败设计成了三级处理机制。

第一级是重试。对于超时类错误,自动重试一到两次,间隔时间递增。幂等性要求较高的操作(比如创建订单、扣款这类不能重复执行的),我们会在参数里额外注入一个幂等键,保证重试不会造成重复操作。

第二级是降级。如果某个技能不可用,可以换成备选方案。比如主订单查询接口超时,走只读缓存查询,虽然延迟高一点但至少有数据。专门负责发送验证码的技能挂了,就自动切换为一体化推送通道。这个思路和微服务里的熔断降级一模一样,只是被应用在了Agent能力层。

第三级是让模型带着错误信息做决策。当前的技能返回了业务错误码,模型继续执行主流程可能会产生错误结果,所以当重试和降级都用不上时,我们会把错误码和错误上下文打包成一个结构化上下文,让模型判断是切换另一个技能,还是直接告诉用户暂时无法处理。

这一级的设计非常关键。错误信息必须结构化。比如:

{ "error_code": "ORDER_NOT_FOUND", "error_context": {"order_id": "SF1234567890"}, "suggestion": "请先向用户确认订单号是否正确,或引导用户通过其他渠道人工查询" }

模型拿到这个结构后,能清楚地知道自己面临的状况和可选的处理路径,而不是对着一个“系统错误”四个字发呆。这基本上就是把“错误处理”的常规划法,平移到了Agent的场景里。

4. 把技能当成团队资产来治理:仓库结构、版本与回归

4.1 技能仓库的目录结构设计

当技能数量从几个增长到几十个、上百个时,光有技术层设计不够,还要有治理层的设计。技能应该像代码一样,有仓库、有版本、有评审、有测试。我们目前使用的技能仓库目录结构如下:

skills/ query_order_status/ SKILL.md # 面向模型的自然语言描述 metadata.yaml # 版本、作者、依赖、权限等元信息 schema.json # input/output 的 JsonSchema 定义 executor.py # 技能的实际执行代码 test_cases.json # 回归测试用例 execute_refund/ SKILL.md metadata.yaml schema.json executor.py test_cases.json ...

这里面三个文件值得单独说明。

第一个是SKILL.md。它和schema.json里的description有什么区别?我们的实践是:schema.json里放的是精炼的、直接注入Prompt的描述,面向模型;SKILL.md是这份技能面向人的完整文档,包括设计动机、使用限制、变更记录、典型调用链。SKILL.md是不会被直接喂给模型的,它是给团队协作用的。

第二个是metadata.yaml。它解决了技能的“户口”问题:

id: skill.query_order_status name: query_order_status version: 2.3.1 owner: fulfillment-team dependencies: - internal-api: order-service@^3.4 permissions: - scope: order.read reason: 需要读取订单状态和物流信息 visibility: public enabled: true

权限这块尤其重要。我们在第四版重构时把所有技能的权限单独拎出来管理,一个技能能做什么、不能做什么固定写在metadata里,由部署时统一注入凭证。这样既方便审计,也避免技能代码里硬编码各种密钥。

第三个是test_cases.json。这是技能回归测试的核心,每个技能都配五到十组“样例输入+期望行为”,每次修改技能定义或者升级模型版本后都要过一遍回归。

[ { "id": "case_001", "input": "我的订单怎么还没发货?", "expected_skill": "query_order_status", "expected_params": {"order_id": null, "user_id": "auto-injected"}, "expect_action": "ask for order_id", "note": "用户未提供订单号,应先索要" }, { "id": "case_002", "input": "查一下订单 SF1234567890 到哪了", "expected_skill": "query_order_status", "expected_params": {"order_id": "SF1234567890", "user_id": "auto-injected"}, "expect_action": "execute_skill", "note": "订单号直接出现在问句中" } ]

4.2 回归测试与“描述漂移”:被大多数团队忽略的隐性风险

技能治理里最容易忽略的是“描述漂移”问题。模型的版本升级以后,对同样一段技能描述的语义理解会发生细微变化。原本在模型A上表现良好的技能描述,换到模型B上触发准确率可能掉20个百分点。但我们很少会主动因为模型升级去重新看技能的触发表现,这就像一个代码仓库里改了函数签名,但因为你没有跑测试,函数挂了都没人知道。

所以每个技能仓库都配了回归测试。有一项测试专门测“路由准确性”:放一组用户的真实表达,看期望被召回的技能有没有出现在候选列表里,期望参数有没有被正确提取。每次模型版本升级或者技能描述改动后,都跑一遍全套回归。

我们内部用了一个简单的持续集成脚本:

def evaluate_skills(skills, test_suite): results = [] for skill in skills: for case in test_suite: expected = case["expected_skill"] routed = route(case["input"], top_k=5) routed_names = [r["skill"]["name"] for r in routed] hit = expected in routed_names results.append({ "case_id": case["id"], "expected": expected, "routed": routed_names, "hit": hit, "params": extract_params(case["input"], routed) }) accuracy = sum(r["hit"] for r in results) / len(results) print(f"路由准确率: {accuracy:.1%}")

这个脚本的价值在于把技能质量的度量变成一个可量化指标。只要路由准确率低于某个阈值,就不允许把技能的改动合并到主干。这听起来很土,但确实是最有效、最基本的质量门禁。

4.3 一致性的价值:为什么说“笨但稳定”胜过“聪明但飘忽”

再说一个关于“技能描述需要持续迭代”的体会。我们在技能迭代上踩过一个比较深的坑:有一次为了提高某个技能的触发率,我们把描述写得特别具体,把所有边角情况全部展开。结果加上去之后,这个技能在核心场景的准确率反而下降了。

原因是描述过于具体之后,模型会把“细节匹配”当成“语义匹配”。真实用户表达千变万化,描述里写越细,模型越倾向于找跟描述字面对得上的表达,而忽略语义上相关但表达方式不同的问法。

后来我们把描述写成了一个金字塔结构:最前面是技能的“核心职责”一句话概括,然后是要覆盖的典型场景,然后是必须避免的误用场景,最后是返回值的处理说明。这个结构的好处是,模型就算只理解了第一句,也能大致知道技能是干什么的。就像给一个人贴一张标签:“退货处理专家——只管退款退货,不管物流查询——遇到拿不准的转人工。”

模型调用的稳定性,靠的是这种既简单又有明确边界的描述。

5. 实测里最容易翻车的四个细节,以及我的修法

5.1 把技能描述写成代码注释,模型根本不知道何时调用

这是给自己看的思路和给模型看的思路的区别。很多工程师写技能描述时,习惯性地写成“查询订单状态接口”这种话,这本质上是给维护者看的代码注释,不是给模型看的调用说明。

模型判断是否调用技能,靠的是用户意图和技能描述之间的语义匹配。如果描述里没有“当用户提到X时使用”这类意图触发词,模型就只能靠猜。我后来形成的习惯是:每个技能描述开头必须有“当用户……时使用本技能”的句式,让模型在这个问题上少做思考。

对比一下两种写法效果差异非常明显。下面这种写法,从12%的误调率降到了4%:

差:查询订单状态接口 好:当用户询问订单物流、发货时间、订单状态、签收情况时,使用本技能查询订单实时状态。不要用本技能处理退货退款,退货退款请走 return_order 技能。若查询返回的状态为 canceled,请直接告知用户订单已取消。

5.2 强行要求模型输出Json,遇到长文本就翻车

我们在早期版本里严格要求模型在调用技能前必须输出完整Json,美其名曰“结构化决策”。真实情况是,一旦模型输出超过几百Token,就有概率Json被截断,或者在里面混入解释性文字。特别是在技能多了之后,模型会把“思考过程”和“输出”混在一起,打印出一大段“我要调用query_order_status,因为用户看起来在问物流……”然后再跟一个残缺Json。

一个非常行之有效的修法:拆开思考和动作。模型只输出一个精简的动作表示,包含技能名和参数。让模型在说出来之前先“憋住”。这在技术上主要是通过在Prompt里硬性规定输出格式,且要求“只允许输出标准Json,不要包含任何多余解释”,并且用解析器做容错。这个方法当然不完美,但对结构化稳定性有明确的提升。

如果你依然觉得模型输出的Json不干净,最保险的办法是在技能执行层再做一次参数校验和类型强制转换,做不到就明确报错,不要让脏数据流向下游。

5.3 技能膨胀以后,互相之间“抢活干”

技能到了几十个量级之后,会出现一个之前完全没预料到的问题:技能之间“抢活”。用户说“我要退掉这个订单”,同时命中了query_order_status、return_order、refund_status三个技能的相似度Top5,模型在三个技能之间犹豫不定,最终选择了影响面最大的那个,结果把流程搞乱。

这个问题要从两个方向治。第一是强化技能边界描述,在返回相关的技能里明确写“不要用本技能处理退货退款”。第二是在路由时引入“排他规则”:当return_order被召回并得分超过阈值时,强制把query_order_status从候选列表里排掉。这类硬规则在关键时刻比模型判断可靠得多。

这个“排他规则”的做法,也可以理解成在技能库里定义了“技能间的协议”。技能不是孤岛,它们之间互相竞争,本身就意味着业务边界需要更清晰地划分。

5.4 技能执行失败后,Agent直接卡死

无论技能写得再好,外部依赖一定会出问题。我们曾经历过一次线上事故:负责发送短信验证码的技能因为第三方服务商限流持续失败,Agent在技能调用上不断重试,导致用户等待时间超过30秒,体验非常差。

复盘后发现三层问题:第一,技能控制的重试次数没有上限,失败后一次接一次地打第三方接口;第二,没有降级方案,验证码发不了就一直卡着;第三,也没有报错给上层编排逻辑,让Agent有机会用别的方式处理。

事后我们给所有技能统一加上了外部调用包装器:

import time from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type(TimeoutError), before_sleep=lambda retry_state: log(f"技能调用超时,第 {retry_state.attempt_number} 次重试") ) def call_external_with_retry(func, *args, request_id=None, **kwargs): return func(*args, **kwargs)

除了重试次数限制,另一个重要习惯是对可重复执行和不可重复执行的操作分开处理。查询类操作随便重试,但涉及创建、退款、修改的操作必须带幂等键,否则一次超时重试就可能产生两次扣款。幂等键我们统一用request_id加操作类型生成,在调用外部接口时作为参数传进去,这样即使网络超时重试,外部系统也能识别出这是同一笔请求而不会重复执行。

这些细节做出来后,技能失败导致的线上事故几乎绝迹。

为了更直观地说明改造前后的差距,我整理了一份我们在客服助手项目上的实测对比数据(基于约2000条真实用户请求抽样):

指标裸用大模型(无技能体系)有技能体系之后
任务完整执行率38.2%86.4%
技能调用准确率71.5%94.8%
平均处理时长4.2秒2.1秒
Json/结构化输出合法率68.0%99.1%
因外部接口失败导致的卡死率12.0%0.3%

这个改善不是来自模型升级,而是来自把模型能做好的事和模型做不好的事分离开来。

6. 从单个Agent到一群Agent,技能体系的演进边界

6.1 技能是Agent的“能力单元”,Agent是技能的“执行岗位”

当技能体系稳定之后,再往上一层就是多Agent的编排。这时我们会发现当初把技能单独抽出来的设计是值得的。因为在一个多Agent系统里,技能是复用的最小单元,而Agent更像是技能的执行岗位,一个Agent挂载一组技能,扮演一种角色。

做一个简单的比喻。技能相当于一个公司的“职能模块”:财务能力、客服能力、运维能力。Agent相当于具体的“岗位”:客服Agent、质检Agent、运营Agent。你也可以让一个Agent同时干好几个岗位的事,但技能模块本身是可以被多个Agent复用的。

比如我们的订单查询技能,既被“客服Agent”用来回答用户物流咨询,也被“运营Agent”用来做仓储物流分析。两个Agent看到的技能是同一个,但是调用场景和历史上下文不同。如果技能不是作为一个独立模块存在,而是直接塞进某个Agent的Prompt里,这种复用根本不可能实现。

所以在技能设计时,我建议大家从一开始就要考虑未来复用的可能性。描述要尽量去掉对特定Agent的依赖,不要写“你是客服助手,你应该……”,而是写“当用户询问物流信息时……”这样通用的触发条件。

6.2 技能体系的演进方向:从“能力模块化”到“标准化协议”

随着技能库越来越大,两个团队维护的技能可能互相需要调用,共享技能库的治理问题就会浮出水面。我们目前正在做的事情是参考MCP这类工具协议,把技能的“描述层”和“执行层”进一步解耦。描述层完全标准化成统一的数据结构,执行层则通过进程内调用、本地HTTP服务、远程服务三种方式暴露给运行时。

这个思路相当于给技能定义了一套通讯协议,不同技能之间可以互相发现、协商、调用。到这一步,技能体系就已经从一个“工程优化手段”变成了“Agent应用的操作系统”。未来内部任何一个新场景,都可以像搭积木一样从能力仓库里挑选技能来组装。

当然,我并不是建议每个团队一上来就奔着这个目标去。技能体系最大的优点是渐进式引入,哪怕你现在只是一个有两三个技能的Demo,只要把“描述层、能力层、编排层”这三层想清楚了,后续的扩展只是规模问题,不是方向问题。

我自己的经验是:先把一个垂直场景做扎实,把三五个技能打磨到调用准确率95%以上,再横向复制到其他场景。技能描述每一次迭代投入的时间,都会在后续复用中加倍赚回来。

Agent这块的玩法变化很快,但技能化的思路应该会持续很长时间。如果你也在折腾Agent,建议从最小的一个业务动作开始,先别急着上大而全的编排平台,用一套目录结构把一两个技能管起来,跑通路由和失败处理,再慢慢把更多能力和流程装进去。做好这一层,你会发现Agent真正开始像一个可靠交付工作的系统,而不是一个彩排完美的Demo。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询