资讯动态

AI Agent技能系统设计:从Prompt到可治理的工程模块

发布时间:2026/10/14 8:54:47 来源:尧图企业网站定制
对现在的AI应用开发者来说**Skills技能**这个词已经快被说烂了。但真正能把它从几个Prompt模板做成一套可治理、可扩展、可排查的工程模块的团队并不多。我前阵子给内部某个智能助手项目搭技能库踩了不少坑也把技能系统的骨架从零趟了一遍。这篇就把我对Skills的理解、目录设计、最小实现、生产环境里的坑一次性说清楚。这篇文章适合两类人一是正在做AI Agent、想给助手或工作流加工具调用能力的开发者二是团队里已经有零零散散的工具函数、但想系统化管理技能的负责人。我会尽量少讲空话多给能直接抄作业的设计思路和代码。1. 把技能从口号变成可落地的工程概念1.1 技能其实是模型与外部世界的中间层先说清楚一件事在Agent系统里技能到底解决什么问题。大模型本身只有文字进、文字出的能力它没法真的查数据库、发请求、算数值、调第三方接口。Skills的定位就是把模型的意图翻译成能实际操作的外部动作再把外部动作的结果翻译回模型能理解的文本。本质上这是一个中间抽象层向前对接大模型的自然语言向后对接确定性的代码逻辑和API。我习惯用一个家政服务的类比来解释大模型就像一个管家技能就是管家手里的一本服务商通讯录。管家根据客人的一句话用户输入决定打电话给保洁、叫维修工还是预约通渠服务商干完活之后管家把结果整理成一句人话汇报给客人。服务商就是技能函数服务商的传单干过什么、擅长什么、收费方式就是技能描述而管家的判断逻辑就是模型调度。为什么要专门搞一个抽象层原因是纯靠把代码写在主流程里根本没法支撑多变的用户场景。今天加一个查天气的技能明天加一个发邮件的技能后天又要换成新的供应商如果都写死在if-else里改一次就要重新发版、重新弹框测试。有了技能系统新增能力就是在技能仓库里多注册一个条目模型在运行时能自动发现和选择系统的扩展成本一下就降下来了。1.2 Prompt技能与代码技能两类完全不同的东西很多人一聊Skills就把技能等同于函数调用这是最常见的第一层误解。实际上在我见过的大多数Agent工程里技能至少分成两类第一类是自然语言技能本质是一段精心设计的Prompt模板或指令片段。它不真正执行代码而是约束模型的输出格式、思考路径或表达风格。比如你要按照三段式结构汇报这个场景下禁止编造数据只能使用提供的数字都属于自然语言技能。它的好处是灵活、不用写代码、改起来快坏处是LLM不是规则引擎同样的描述在不同上下文里可能表现飘忽。第二类是确定性代码技能也就是真正可以执行的动作比如Python函数、API调用、Shell命令、SQL查询。它有明确的输入输出结果可验证、可测、可审计。坏处是每个技能都写代码工作量大得多。我见过不少团队把所有东西都堆进第一种技能里认为反正模型很聪明写清楚就行。结果是系统在Demo阶段跑得很顺一上生产就频繁出幺蛾子——因为自然语言技能在长上下文里很容易被稀释模型会遗漏某些约束条件。最终成熟的做法是混合式的凡是确定性的、可以被代码验证的操作一律沉淀成代码技能并且把它们包装成工具接口凡是涉及表达风格、输出格式、推理步骤的约束才考虑用Prompt技能。这样划分之后系统里真正不可控的部分就大大缩小了。下面是我自己参考过的技能类型选型表初建技能库时可以直接当判断依据判断维度自然语言技能代码技能是否真正执行外部动作否只影响模型输出是会调用函数/API确定性低结果有概率波动高同一输入同一输出开发成本低改一句话即可高需要写逻辑和DEBUG适用场景输出格式、思考模板、禁忌提醒查数据、算结果、发请求、操作文件适合放在哪里Prompt总则或技能描述里独立的技能注册表/工具库2. 技能库怎么拆目录、命名与描述的设计规范2.1 我的教训按业务域拆目录而不是按实现语言拆技能库最致命的一个设计错误是一开始就按技术手段来建目录。比如把所有涉及HTTP请求的放一个文件夹所有涉及文件操作的放另一个文件夹理由是这样代码复用方便。结果就是模型在选技能时根本不知道发个订单消息给我应该找HTTP工具目录还是消息通知目录——因为业务语义和技术实现被硬拆开了。我后来把技能目录完全改成按**业务域Domain**来切分。订单管理域下放创建订单、查询订单状态、取消订单客户管理域下放新增客户、查询客户资料、更新联系方式内容生成域下放生成周报草稿、总结会议纪要、改写文案。一个技能只能归属一个域不许跨目录复用如果两个域都要用同一个底层函数就把公共逻辑往下沉一层写成基础设施能力而不是把技能本身做成交叉引用。这样建出来的目录结构大概是skills/ ├── order/ │ ├── create_order.py │ ├── query_order_status.py │ └── cancel_order.py ├── customer/ │ ├── create_customer.py │ ├── query_customer.py │ └── update_contact.py ├── content/ │ ├── generate_report_draft.py │ ├── summarize_meeting.py │ └── rewrite_text.py └── common/ ├── http_client.py └── auth_service.py这套结构的好处有三个第一新增业务技能的定位很清楚往对应文件夹丢一个文件就行第二模型在收到请求时能凭业务语义快速缩小候选技能的范围第三权限和审计也能按域批量设置——比如内容生成域的技能可以允许大模型自由调用但订单管理域的技能要求额外鉴权。2.2 命名、描述、参数schema的标准写法目录定好了接下来就是单个技能的最关键三件套函数名、描述、参数schema。这三样东西直接决定了模型能不能在正确的时间选中正确的技能。我踩过一次典型的坑两个技能一个是query_order_status另一个是query_delivery_info描述都写得模模糊糊模型经常在用户问我的货到哪了的时候先调了订单状态查询再调物流查询白白浪费一轮调用和用户等待时间。先说命名。我推荐一律动词开头、下划线分隔的短驼峰比如create_order、convert_pdf_to_text、list_unread_emails。为什么不用纯名词因为技能本质上是一个动作动词开头能让模型的语义理解更直接。也不要为了省字符写成qos、crt_odr这种缩写模型不认识反而增加误判概率。然后是描述。描述写得好不好是决定技能被使用频率的胜负手。我最终沉淀出三段式模板这个技能做什么一句话动词宾语结果什么时候应该用它给出典型触发场景什么时候不要用它反例和边界这一步极其重要。给个例子比干讲理论直观得多def query_order_status(order_id: str) - str: 查询订单的当前状态返回最新进度信息。 当用户询问我的订单到哪了发货没有签收没有时使用。 不要用于查询包裹物流轨迹那是 query_delivery_info 的职责。 如果订单号不存在返回 NOT_FOUND 而不是抛异常。 ...这段描述里最值钱的就是那句不要用于……。加了反例描述之后我测试的误选率下降了大概三成这个收益是实打实的。最后是参数schema。在给模型看的工具定义里每个参数至少要写清楚类型、是否必填、枚举范围、示例值。我见过太多人只写order_id: str结果模型传进来一个带空格的订单号或者传了一个全角冒号开头的内容。参数schema里给一个示例值比如order_id示例SO-20240615-001模型就能正确模仿格式这比在函数内部拼命做兼容省心得多。3. 自己动手搭一个最小技能注册与调度系统3.1 核心代码用装饰器做一个技能注册表最小可用的技能系统不一定要上重框架。我建议先用Python的装饰器和一个全局注册表把所有技能统一管理起来。这样做的核心逻辑是让技能函数自己声明自己是技能而不是靠一个额外配置文件来登记。用装饰器的原因是就近维护——函数的定义处就是它的声明处不会出现配置里写的技能名和代码里实际函数名不一致的情况。下面是我实际跑通过的最小版代码# registry.py from typing import Callable, Dict, Any SKILL_REGISTRY: Dict[str, dict] {} def skill(name: str, description: str, parameters: dict None, scopes: list None): def decorator(func: Callable) - Callable: SKILL_REGISTRY[name] { name: name, description: description, parameters: parameters or {}, scopes: scopes or [], func: func } return func return decorator然后每个技能文件里这样做声明# skills/order/query_order_status.py from registry import skill skill( namequery_order_status, description查询订单的当前状态返回最新进度信息。 当用户询问订单进度、发货、签收时使用。 物流轨迹请用 query_delivery_info。, parameters{ order_id: { type: string, required: True, example: SO-20240615-001 } }, scopes[order:read] ) def query_order_status(order_id: str) - str: # 这里是实际查询逻辑返回给模型的文本 return f订单 {order_id} 当前状态已发货预计明日送达。所有技能文件只要在程序启动时被import一次就自动进到SKILL_REGISTRY里。我会在入口文件里用pkgutil遍历加载skills目录下所有模块避免每次新增技能都要手动改import列表。3.2 调度逻辑模型怎么知道要调哪个技能注册表有了接下来是调度。现在主流的做法是基于大模型的函数调用能力也就是把整个技能注册表转换成模型API里的tools参数让模型在生成回答的同时输出一个结构化的我要调用哪个函数、参数是什么。伪代码大概是这样的# dispatcher.py def dispatch(user_message: str) - str: # 1. 组装给模型的 tools 定义 tools [ { type: function, function: { name: info[name], description: info[description], parameters: { type: object, properties: info[parameters], } } } for info in SKILL_REGISTRY.values() ] # 2. 请求模型让它决定是否调用技能 response llm.chat( messages[{role: user, content: user_message}], toolstools, tool_choiceauto ) # 3. 检查模型是否想调用技能 tool_calls response.tool_calls if not tool_calls: return response.content # 4. 执行对应的技能函数 results [] for call in tool_calls: skill_name call.function.name args json.loads(call.function.arguments) info SKILL_REGISTRY.get(skill_name) if not info: results.append(f错误未知技能 {skill_name}) continue try: result info[func](**args) results.append(result) except Exception as exc: results.append(f错误技能 {skill_name} 执行失败 - {exc}) # 5. 把执行结果回传给模型让模型生成最终回答 final llm.chat(messages[ {role: user, content: user_message}, {role: assistant, content: None, tool_calls: tool_calls}, {role: tool, content: \n.join(results), tool_call_id: tool_calls[0].id} ]) return final.content这段逻辑里有一个细节值得单独说工具返回值必须是字符串。很多模型推理是靠文本流来理解上下文的如果你返回一个Python字典模型很可能只记住dict这个字面意思而丢失关键字段。我习惯把所有技能函数的返回值都做成一段完整的、自包含的文本类似订单 SO-20240615-001 当前状态已发货预计明日送达。这样模型拿到的就是一个可以直接被引用的干报告而不是还要自己解析一堆结构。3.3 上下文管理技能多了token会被工具描述吃满技能系统不会永远只有五个技能。当技能数量超过三十个tools参数本身的token开销就会非常明显。我遇到过最夸张的情况三十多个技能的完整描述加起来快一万token一次对话还没开始上下文就被占掉了四分之一。解决思路有两个一是按意图做技能预备筛选。在把全部技能发给模型之前先用一个轻量分类器可以用更小的模型或者正则规则判断用户请求大概属于哪个领域只把该领域下的技能装进tools参数。二是给技能描述定期瘦身把实际调用率连续一段时间为零的技能先下线需要的时候再启用。这里也要提醒一个容易踩的坑function calling模式下temperature不要调太高。我实测下来temperature超过0.7之后模型开始偶尔自由发挥编造参数。我们内部现在统一跑Agent的主链路都把temperature压在0.2以下宁可回答保守一点也不要让它瞎编工具入参。4. 压测和生产环境里真实踩过的坑4.1 技能描述失效模型开始乱选工具第一次报警是在压测阶段。当时系统里有查天气和查空气质量两个技能单独测都正常但用户连续问今天适合户外跑步吗时模型有一半概率调错技能——它选了天气技能却传了本该给空气质量技能的参数然后得到错误结果就重试一来一回延迟飙到十几秒。排查的时候我做了三件事。第一步翻调用日志看模型每次到底选了哪个技能、传了什么参数。第二步把两个技能的描述并排摆在桌面上比对发现它们的开头几乎一模一样都是查询当前城市……。第三步我仔细想了想用户问题的本质——适合户外跑步吗其实同时需要天气、空气质量和紫外线三个维度单独靠一个技能根本无法回答。最终修复不是简单改描述而是加了一个check_outdoor_running_suitability聚合技能内部同时调用三个数据源一次返回完整结论同时把旧技能描述里的触发场景写得更加狭窄。这个案例给我的启发是很多模型选错工具的问题根源不是模型笨而是你的技能边界设计得不够清晰把复合问题硬掰成了单选。4.2 参数冲突与并发竞争共享资源被多个技能同时改技能执行往往比模型生成更危险因为它是真正在操作外部系统。我们曾经遇到过一个故障用户同时问了帮我查一下A订单创建时间和帮我查一下B订单创建时间两个技能几乎同时执行都去调同一个内部认证服务刷新token。结果刷新逻辑没有加锁两个线程都拿到旧的token一个把token踢下线另一个带着过期token发起请求直接401。整个下午订单查询接口的失败率飙到40%。这类问题的排查链路比较典型先看错误日志发现大量401再看调用链发现并发到达认证接口最后看代码发现共享的token缓存没有加互斥。最终我把每个技能访问共享资源时都强制走一层资源代理层token刷新做成单飞模式single-flight也就是同时只有一个请求真正去刷新其他请求等结果复用。另外还加了一道防线所有技能函数一律用try-except包裹任何异常都转换成标准化的错误文本返回给模型绝不把裸异常抛到调度层。注意技能函数内部吞掉异常并返回错误文本是给模型纠错机会的必要设计。如果调度层一收到Exception就中断模型连重试或换个技能的可能都没有。4.3 上下文窗口被工具描述吃满一个真实的数据复盘有一段时间我们的Agent响应质量明显下降调查之后发现并不是模型变笨了而是tools参数里的技能描述实在太多太长导致模型在生成最终回答时记不住用户原始问题的关键信息。我统计了一下二十五个技能平均每个描述一百五十个汉字加上参数schema累计消耗约三千五百token用户一轮对话的正常上下文才几千token等于模型一半的注意力都被工具说明占用。修复方案是把技能描述改得更紧凑同时给每个技能增加一个摘要字段只有在前置筛选命中之后才加载完整描述前置筛选阶段只传摘要每个技能只占几十token。改动之后单轮对话总token下降40%响应速度明显提升。5. 技能系统的下一步权限、审计、缓存与热更新5.1 权限最小化技能越强大越需要看门狗技能直接操作真实系统权限设计就要当成一个独立的安全模块来对待。我给每个技能声明所需的最小权限范围比如order:read、order:write、customer:write在注册表里加上scopes字段。调度层在做工具调用前会先校验当前会话的权限集没有对应权限就拒绝执行并把拒绝原因返回给模型。这里有个容易被忽略的细节权限校验必须在技能函数执行之前做而不是在函数内部做。如果放在函数内部很容易出现某一段代码忘了校验就放行的情况放在调度层统一处理无论将来加多少个技能都绕不过这一道闸。我在代码示例里已经写了scopes字段正式环境只要在dispatch函数里补上会话权限校验即可。5.2 审计日志没有日志技能系统出问题就是玄学技能系统比普通后端代码更玄学的地方在于同样的输入模型可能这次调A技能下次调B技能。所以审计日志不能只记录技能执行结果还要记录模型决策上下文。我们最后落地的结构化日志字段是这样的字段说明session_id会话ID串起多轮上下文user_input用户原始请求selected_skill模型最终选择的技能candidate_skills前置筛选时给了哪些候选技能arguments传给技能的具体参数result技能返回结果或错误latency_ms技能执行耗时decision_context_tags模型决策相关的标签意图分类、命中的规则等有了这些字段排查为什么模型这次没选对技能就不再是猜直接对比正常请求和异常请求的决策上下文即可。5.3 热更新与灰度让技能跟上业务变化而不是随主流程发版技能的迭代速度通常比主程序快得多尤其是自然语言描述部分经常会因为模型效果波动而微调。强制把技能和整个Agent服务一起发版会让上线粒度变得很粗一天可能要发好几版。我们的做法是技能描述、参数schema这类元信息放在配置中心管理程序启动时和运行中定时拉取技能代码本身随服务发版。这样改一句描述可以直接通过配置中心下发五分钟生效不用重启服务。试运行阶段先灰度给少量会话观察技能调用成功率和用户好评率稳定后再全量放开。还有一点非常值得投入给技能加版本号并在审计日志里记录版本信息。否则当技能行为突然变化时你根本无法判断是代码问题还是描述配置被改了。这里分享一个我最后想强调的经验给每个技能写何时不要用的反例描述看起来是最不起眼的一步实际收益却最大。大多数技能选错问题都不是模型不够聪明而是我们在描述里只告诉模型它是什么却没告诉模型它不是什么。技能系统难在边界设计只要把边界划清楚模型就会稳定很多。下次你改完技能描述记得拿一批历史对话跑回归看看误选率到底降没降。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑