技能驱动架构:让智能体从混乱走向可控的工程实践
2026/9/23 6:09:33 网站建设 项目流程

我从去年开始密集接触智能体项目,前后帮三个团队搭过 Agent 底座。一个很明显的共性问题:大家一开始都把 Agent 当成一个超大的函数来写,所有逻辑堆在一个文件里,prompt 越加越长,工具函数越挂越多,到最后没人说得清这个 Agent 到底能干什么、不能干什么。直到我把手头一个内部项目重构成“技能驱动”架构,局面才彻底改变。这个项目的名字就叫 agent-skills,核心思路只有一句话:把智能体的能力拆成一个个可以被声明、注册、编排、观测的技能单元,让大模型只负责“选技能、填参数”,把“执行”交还给确定性的代码。这篇文章从设计原则讲到工程落地,再到上线后踩过的坑,完整复盘一遍。适合正在做 Agent 落地的工程师、技术负责人,以及准备把智能体接入业务系统的团队参考。

1. 为什么我坚持把智能体改造成“技能驱动”架构

很多团队问我的第一句话是:Agent 框架不是现成的吗?LangChain、Semantic Kernel、各类编排框架,拖进来就能跑,为什么还要自己做一套技能层。我的回答一般是:框架能帮你省掉重复代码,但省不掉架构决策。你的 Agent 如果只有三个技能,用框架自带的能力没有问题;当技能超过三十个、由不同团队维护、要按业务线做权限隔离的时候,你就必须有一个统一的能力抽象层。agent-skills 就是在这个前提下开始做的。

1.1 复盘踩过的坑:智能体项目最大的问题不是模型不够聪明

先复盘一下我最早做的那个失败版本。当时的需求是做一个内部客服助手,能查订单、查物流、处理退换货。我按“万能 Agent”的思路,把用户问题直接丢给大模型,system prompt 里写了十几条业务规则,又给模型挂了七八个函数。demo 演示效果非常好,什么问题都能答。一上线就崩了:

  • 模型把订单号识别错,把 0 认成 O,然后在订单系统里查到不存在的数据;
  • 两个技能都需要调用“查询用户身份”的逻辑,但一个技能里写死了用户 ID,另一个靠模型猜,结果权限校验逻辑重复且不一致;
  • prompt 越来越长,技能描述互相干扰,模型开始把“查物流”的功能安到“查订单”技能上。

最典型的一个线上事故:用户问“我的快递什么时候到”,模型正确选中了物流查询技能,但参数里把tracking_numberorder_id混在一起填,传了一个既不是订单号也不是运单号的字符串。由于下游接口做了模糊匹配,居然返回了一个陌生人的包裹信息。这个事故给了我两个教训:第一,模型输出不可作为最终事实,参数必须经过严格校验和归一化;第二,技能的边界必须清晰,不能让模型靠猜来区分两个相似的技能。agent-skills 就是在这些事故之后重写的架构。

1.2 技能驱动架构的核心思路

技能驱动架构的本质是把“Agent 会什么”这件事从隐性的 prompt 中抽出来,变成显性的、可管理的代码资产。每个技能是一个独立单元,包含三样东西:技能的元信息描述(告诉模型这个技能何时用、怎么用)、参数的 Schema(约束模型填参数的结构)、以及执行逻辑(真正干活的代码,不被模型直接触碰)。

这样一来,Agent 的主循环变得很薄:接收用户输入 -> 结合当前上下文挑选技能 -> 生成结构化的调用参数 -> 交给技能层执行 -> 把结果格式化成自然语言返回。模型的职责被收窄到“决策”这一层,具体执行全部落在确定性的代码里。这个架构对我最大的价值是可以量化:我能数出当前系统有多少技能、每个技能被调用了多少次、成功率和失败率是多少,而不是靠感觉去猜 Agent 好不好用。

1.3 agent-skills 的定位和设计原则

我给 agent-skills 定了四条设计原则,后文的实现都围绕这四条展开:

  • 技能描述和技能实现分离。业务代码不需要知道模型怎么描述它,模型看到的描述由独立文件管理,方便算法同学单独调参。
  • 一切可观测。每次技能调用都必须留痕,包括入参、出参、耗时、token 消耗、异常信息。
  • 失败必须显式。技能执行失败时返回结构化错误信息,模型可以根据错误信息决定重试、换参数还是向用户坦白,而不是生成一段含糊的“系统开小差”。
  • 先本地后远端。所有技能先以 Python 函数的形式存在本地,跑通之后再封装成微服务,避免一上来就搞分布式。

有了这四条原则,后面的代码和目录设计基本就是水到渠成的事情。

2. 技能定义:一份让模型和代码都能读懂的契约

技能是整个架构的核心资产,技能定义的质量直接决定模型调用的准确性。我见过太多团队把技能定义写成一大段说明书式的文字,模型读完之后依然不知道什么时候该用这个技能。agent-skills 里,技能定义被拆成三层:面向人的文档、面向模型的描述、面向代码的 Schema。

2.1 三层结构的设计与职责

第一层是SKILL.md,给人看的。它描述技能的业务背景、前置条件、依赖的权限、可能返回的数据样例。这层内容不进 prompt、不参与模型推理,它服务于团队协作,新成员通过读这层文档就能理解技能是干什么的。

第二层是 skill description,给模型看的。它必须克制、精准,只说“什么场景下用”和“关键注意点”。我在实践中发现,描述写得越短,模型选得越准;描述里一旦出现“通常”“可能”“一般”这类模糊词,模型就会出现选择漂移。一条好的 description 是确定性的陈述句,例如:“当用户询问订单物流状态并提供订单号时使用”,而不是“这个技能可以查询订单的物流信息,也可能用于其他相关场景”。

第三层是 params schema,给代码和模型共同看的。它用 JSON Schema 描述参数结构,代码用它做校验,模型用它做参数生成引导。Schema 是技能契约的硬边界,模型再怎么发挥,最终都要落进这个结构里。

QUERY_DELIVERY_SCHEMA = { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式为 OD 开头加 14 位数字,例如 OD20250101000123", "pattern": "^OD\\d{14}$" }, "include_trace": { "type": "boolean", "description": "是否返回完整物流轨迹,默认 false,只返回当前节点", "default": False } }, "required": ["order_id"], "additionalProperties": False }

2.2 参数 Schema:把自由文本变成结构化输入

参数 Schema 的重要性怎么强调都不为过。它决定了模型从用户一句自然语言里抽取出什么样的结构化信息。我第一次设计 Schema 时犯的错是把 description 写得太抽象,比如“order_id 是订单的唯一标识”,模型并不知道这个标识长什么样。后来我养成一个习惯:每个字段的 description 里必须包含一个真实示例,并且把格式规则写进去,例如订单号必须以OD开头、运单号通常是 12 到 15 位数字。

一个我强烈建议开启的选项是additionalProperties: False。如果不加这个约束,模型有时会自作主张往参数里塞一个未定义的字段,比如把user_id混进来。开启了严格模式之后,模型生成的参数任何多余字段都会被校验器拒绝,错误信息会回传给模型,模型会自动修正。根据我的统计,开启 strict 模式后,参数生成合法率从 76% 提升到了 92%,提升非常可观。

还有一个小技巧是枚举字段尽量用 enum 而不是自由字符串。比如查询类型query_type,如果你写 “string 类型,表示查询的是物流还是订单”,模型可能会填 “logistics”“delivery”“shipment”“物流” 各种变体。直接用 enum["order", "delivery"]配合 description 里的中文说明,模型基本不会再填错。

2.3 技能注册装饰器与入口封装

在 Python 实现里,我用装饰器把技能定义和实现绑定在一起。这个模式的好处是技能编写者只需要关注函数体,不需要理解注册中心的内部逻辑。

@skill.register( name="query_delivery", description="当用户询问订单物流状态并提供订单号时使用,返回当前物流节点和预计送达时间", params_schema=QUERY_DELIVERY_SCHEMA ) def query_delivery(params: dict, context: SkillContext) -> SkillResult: order_id = params["order_id"] trace = delivery_repo.get_by_order(order_id) if not trace: return SkillResult.fail(code="ORDER_NOT_FOUND", message="订单不存在或没有物流信息") return SkillResult.ok(data={ "current_node": trace.current_node, "eta": trace.eta, "trace": trace.nodes if params.get("include_trace") else None })

这里有个细节值得展开:SkillContext 是上下文对象,包含用户身份、trace_id、会话状态等系统信息。它由框架注入,不允许技能自己从外部拿全局变量,这保证了技能的可测试性和可移植性。每个技能在测试时只需要构造一个 DataBuilder 伪造上下文,不需要起服务、不需要连数据库。

3. 技能注册与发现:别再用 if-else 管理你的技能列表

技能多了以后,最大的工程挑战不是写技能,而是管理技能。我在早期版本里用过一个字典把所有技能函数列出来,再手写一个 mapping 表。那个维护成本实在太痛了,每加一个技能要动三四个文件。agent-skills 改成基于目录约定和装饰器注册的机制之后,加一个新技能只需要在skills/目录下新建一个子目录,框架启动时自动扫描、自动注册。

3.1 目录规范和自动发现

我采用的目录结构如下:

skills/ query_delivery/ SKILL.md schema.json __init__.py query_order/ SKILL.md schema.json __init__.py send_message/ SKILL.md schema.json __init__.py

框架启动时遍历skills/目录,对每个子目录做三件事:

  1. 读取schema.jsonSKILL.md,加载技能元信息;
  2. 导入__init__.py模块,执行模块内的装饰器注册逻辑;
  3. 校验元信息是否完整:名称是否合法、description 是否为空、schema 是否通过 JSON Schema 自校验。

自动发现解决了“技能越多越乱”的问题,但它也引入了一个隐患:隐式加载导致 IDE 跳转困难。为了兼顾可调试性,我在注册中心保留了一个list_skills()接口,同时启动时会打印一张技能清单,包含技能名、加载状态、描述摘要。这样既享受了自动发现的便利,也保住了排查问题的手段。

3.2 注册时的强校验

注册不只是把函数塞进字典里。build 阶段,我还会做一次模拟调用测试,用 sample params 跑一遍技能函数,确保导入路径没问题、依赖注入能解析。这一步在 CI 里执行,任何技能加载失败都会让流水线红掉,而不是等线上模型调用时才炸。

强校验里的一个重要项是参数名冲突检查。技能之间可能共用底层依赖,但参数名必须显式声明。我曾经遇到两个技能都定义了user_id,但语义完全不同:一个指电商平台的用户 ID,一个指内部工号。模型看到同名字段会串味。agent-skills 的注册中心会扫描所有技能的参数名,遇到同名不同描述的情况直接报错,强制开发者区分字段名,比如改成platform_user_idemp_user_id

3.3 注册中心的查询接口

注册中心对外暴露两个核心查询接口:一个是模型调用时用的match_skills(query),输入用户的原始问题,返回候选技能列表;另一个是面向业务方的get_skill(name),通过名称精确定位。

match_skills的实现在我项目早期是用向量检索,后来发现大部分场景下关键词召回加规则过滤就已经足够。真正的语义匹配交给大模型在推理阶段做,检索只需要把明显不相关的技能过滤掉,缩小候选范围,减少 prompt 长度。这样做的收益很直接:prompt 里塞的技能描述越少,模型的选择准确率越高,token 消耗也越低。

def match_skills(query: str) -> list[SkillSpec]: candidates = [] for spec in skill_registry.all(): keywords = spec.keywords if any(k in query for k in keywords): candidates.append(spec) return candidates[:MAX_CANDIDATE_COUNT]

4. 技能编排:不能只会调用单个技能

技能注册和发现解决的是“模型选对技能”的问题。但真实业务往往需要多个技能协作才能完成,比如收到一个售后请求,Agent 需要先查订单、再查物流、再判断是否在退换货窗口期、最后给用户发送一条售后通知。这中间就有编排问题。我在 agent-skills 里把编排分为三种模式:顺序编排、条件编排、并行编排。

4.1 三种编排模式及落地场景

顺序编排是最普遍的模式,前一个技能的输出作为后一个技能的输入。在我的项目里主要用在一个跨多个系统的查询场景:先通过resolve_user技能拿到用户在各系统的 ID,再拿这些 ID 去调query_orderquery_delivery。顺序编排的关键是定义好技能间传递的数据结构,不能让技能直接从全局 context 里捞数据。

条件编排是根据技能执行结果决定下一步走向。比如check_return_eligibility返回eligible=false时,Agent 应停止后续流程并告知用户原因,而不是继续调create_return_order。我在编排引擎里支持了on_successon_failon_judgment三种分支,分支条件用简单的规则表达式声明,不用写复杂的状态机代码。

并行编排用在彼此无依赖的查询场景,比如用户同时问“我的订单到哪了”和“这个订单花了多少钱”,两个技能可以并行执行。我最早用的是 Python 的asyncio.gather,但很快发现在高并发下需要控制并发度,否则下游数据库连接池会被打满。后来改成了带信号量的并发池,限制最大并行数为 5,明显稳了很多。

4.2 上下文传递与数据隔离

编排最容易出错的地方是上下文传递。我定了一个规矩:技能之间不直接传数据,统一通过上下文对象传递,每条数据都要带来源技能名和写入时间。

class SkillContext: def __init__(self, trace_id: str, user: UserContext): self.trace_id = trace_id self.user = user self._data: dict[str, ScopedValue] = {} def put(self, key: str, value, source_skill: str): self._data[key] = ScopedValue(value=value, source=source_skill, ts=time.time()) def get(self, key: str): if key not in self._data: return None return self._data[key].value

这个设计避免了两个问题:一是数据覆盖,两个技能写同一个 key 时,后写的不一定是对的,有了来源字段可以追溯;二是脏读,编排引擎在分支并行执行时,给每个分支提供 context 的快照,防止分支 A 修改的数据被分支 B 错误读取。

4.3 技能冲突与兜底机制

编排还面临一个模型层面的冲突问题:模型在一步决策时选择了技能 A,但根据业务规则应该选技能 B。比如业务规定,用户申请售后时必须先经过智能客服前置沟通,不能直接创建售后单。这个规则如果只靠模型自觉,一定会翻车。我的做法是在编排层做业务规则前置检查,如果技能调用违反了规则,编排引擎直接返回RULE_BLOCKED错误码,并附带规则说明,模型看到这个错误码后会自动调整策略,向用户解释并引导到正确流程。

这个兜底机制很重要,它把“靠模型自觉”变成了“靠引擎强制”。即使模型在某个边缘 case 上选错了,引擎也会在最后一道闸门拦住,避免向用户展示错误的操作结果。

5. 上线后踩过的坑:超时、上下文污染和幻觉参数

任何架构在落地过程中都会暴露问题,agent-skills 也没有例外。这一节我挑三个最有代表性的坑,还原它们的排查链路和修复方案,给后来者提个醒。

5.1 超时链路排查全过程:模型和技能执行必须分开设置超时

项目上线第二周,线上监控开始出现大面积的超时告警。第一反应是技能函数执行太慢,但查了链路日志发现query_order的平均延迟只有 120ms,不算慢。真正的问题是 Agent 主循环的超时设置:我把整个 Agent 调用链路的超时设成了 15 秒,其中大模型推理稳稳吃掉 8 到 10 秒,留给技能执行的时间只剩 5 秒。如果这轮模型输出恰好选了两个技能并行执行,加上下游系统抖动,5 秒根本不够。

排查过程是这样推进的:先看告警集中在哪个环节,发现集中在“生成回复”阶段;于是打开 trace,给模型调用和技能调用分别打了耗时标签;对比之后发现技能执行本身健康,但整条链路的超时配置太粗放。修复方案很明确——把超时拆成三段:模型推理超时、技能执行超时、总时长超时,分别设置 12 秒、8 秒和 20 秒。拆完之后超时告警下降了 80%。这个改动听着很小,但如果不把“等待模型”和“执行技能”分开看,很多超时问题永远定位不到。

5.2 上下文污染的根因与修复

第二个坑是上下文污染。技能多了以后,我把所有技能的 description 都塞进系统提示词里,导致上下文长度持续膨胀。随之而来的是模型选择准确率下降,特别是技能之间语义相近时,模型会出现“抢答”现象,把本应选 A 的场景选成 B。

根因在于我没有做候选技能的筛选,让模型在一二十个技能描述里做选择,选择空间太大。修复办法就是我前面提到的match_skills预筛机制,先通过关键词把候选技能压缩到 3 到 5 个,再让模型做精细选择。压缩后上下文变短,模型注意力更集中,选择准确率显著回升。我还设置了一个告警:如果单次请求的 system prompt 超过 4000 token,就触发告警,提醒该做技能裁剪了。

5.3 大模型幻觉参数的拦截与修正

第三个坑是最难对付的,因为它的表现不是报错,而是“错得一本正经”。模型在生成参数时,会编造一个符合格式但实际不存在的订单号,或者把日期填成 2024 年 2 月 30 日这种不存在的日期。这类幻觉参数如果不被拦截,就会直接打到下游系统,造成脏数据。

我的处理是双层防线。第一层是参数强校验,在进入技能函数前用 jsonschema 校验格式、枚举、范围,pattern正则能拦掉大部分格式错误的参数。第二层是业务语义校验,比如订单号是否符合当天的单号段、日期是否在合理范围内、金额是否超过订单总额。业务校验规则写在每个技能内部的pre_execute钩子里,一旦校验失败,返回结构化错误码,模型会根据错误信息重新生成参数。经过这两层防线,幻觉参数到达下游的比例从最初的 8% 降到了 0.2% 以下。

6. 可观测性:没有度量就没有优化

智能体项目的调试难度远高于普通后端服务,因为它有一条“模型输出”的不确定环节,复现问题往往是概率性的。如果日志里没有记录模型当时看到了什么、选了哪个技能、填了什么参数,几乎不可能定位到根因。所以 agent-skills 从第一天起就把可观测性当成一等公民来做。

6.1 技能级日志的规范格式

我给每次技能调用定义了统一的结构化日志格式,使用 JSON 输出,包含以下字段:

  • trace_id:一次用户请求的全局唯一 ID;
  • skill:技能名;
  • params:模型生成的参数字段,必须脱敏;
  • result_status:success / fail / blocked;
  • latency_ms:技能执行耗时;
  • tokens:本次调用的模型 token 消耗数;
  • model:本次调用使用的大模型版本。

为什么要定义统一格式?因为跨团队协作时,算法要看模型行为,后端要看业务数据,SRE 要看性能和稳定性。一份统一的日志能支持所有人的需求。我见过很多团队在调智能体问题时,日志散落在各种业务系统里,格式五花八门,最后只能靠人肉对时间线,那种效率太低了。

6.2 建立技能级评估集

除了日志,我还维护了一份技能评估集,也就是一批“输入 -> 期望输出”的测试用例。比如对于query_delivery,评估集里有用户直接问“我的快递到哪了”、用户提供订单号“OD20250101000123”、用户提供了格式错误的单号等案例。每次改技能描述、调参数 Schema 后,我会用评估集跑一遍回归,观察模型选择技能和生成参数的正确率。

这个评估集是活文档,每次线上出了选择错误的问题,我都会把对应 case 加进去,作为一个永久回归项。有一次我发现模型总是把“取消订单”误判为“退货申请”,就是因为之前没有这些边界 case。加入评估集,再调整 description 和参数 Schema 之后,这个错误就不再出现了。

6.3 运行指标的三个核心信号

日志和评估集解决的是“对不对”的问题,运行指标解决的是“稳不稳”的问题。我在 dashboard 上重点盯三个指标:技能调用成功率、参数生成合法率、端到端平均延迟。这三个指标基本能反映 Agent 系统的整体健康度。成功率下降,优先检查下游依赖;参数合法率下降,优先检查最近是否有技能描述改动;延迟爬升,优先检查提示词长度和候选技能数量。没有这三个指标,任何优化都像是在黑盒里猜。

7. 给准备做技能层的团队几点实在建议

最后分享一些从项目里沉淀下来的团队协作和演进建议。技能层不是一日建成的,也没必要一开始就把所有技能都抽象得干干净净。

7.1 先别急着做平台

我在项目初期犯过一个错误:花了两周时间搭技能管理后台,又是审批流、又是可视化编排、又是权限管理,结果真正的技能只有三个。后来我把这套后台全部推倒,先用代码和目录结构把流程跑通,等技能数量上到两位数再重建管理平台。我的建议是:最少可用是一份目录规范加一个注册中心,足够支撑到三四十个技能,不用过早进入平台化。

7.2 技能拆分粒度要跟着业务走

技能粒度过细,比如“查询用户手机号”和“查询用户邮箱”各是一个技能,会导致模型选择时注意力分散;粒度过粗,比如一个“处理售后”技能内部塞了十几步逻辑,又回到了万能函数的泥潭。我的经验是按“一个业务动作”来拆分:查订单、查物流、创建退款单、发送消息、校验资格。每个技能的输入输出边界清晰,且内部逻辑能在一个函数内完成最佳。

7.3 让写技能的人参与线上问题追踪

技能不是写完上线就完了。agent-skills 项目里,我要求每个技能的维护者必须订阅该技能的告警日志,第一次被线上调用失败时,维护者要在 24 小时内给出原因说明。这个机制倒逼技能描述写得更加严谨,因为写得不清楚、参数 Schema 有漏洞,最终由自己承担责任。有个团队按这个方式跑了三个月,技能一次通过率明显提升,模型选择错误的工单量下降了六成。

我在实际项目里最深的一个体会是:技能层真正解决的,是把“大模型不可控”这头大象切成小块,每一块都有明确的输入输出、有校验、有日志、有负责人。模型在这个架构里变成了一个有边界的决策器,它需要做的判断变少了,但判断质量显著提高了。如果你也在为 Agent 的混乱状态头疼,不妨先把当前的能力抽象成技能清单,看看到底有几项业务动作是重复实现的、哪些边界是模糊的,这比追着热门框架跑要实在得多。

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

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

立即咨询