ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Agent技能设计:从Function Calling到可插拔工具调用体系

AI Agent技能设计:从Function Calling到可插拔工具调用体系 最近一两年AI Agent 的落地方式变化很快。去年大家还在讨论怎么让模型记住对话、怎么接一个函数调用现在讨论最多的反而是另一件事给 Agent 准备哪些“技能”。这个词看起来简单但实际做起来坑非常多尤其是当你面对几十甚至上百个业务动作时如何把它们组织好、让模型真的会调用、调用之后出错了能自己纠正这里面的设计空间远比想象中大。我基于 agent-skills 这个方向结合我自己踩过的坑把一套比较完整的做法拆开讲讲。这篇内容适合已经在做智能体应用、或者准备把大模型接入真实业务系统的朋友不适合只想跑通 demo 的读者——因为这里讨论的重点不是“怎么让模型说话”而是“怎么让模型干活”。1. 先从整体的技能框架讲起1.1 为什么需要“技能”这个概念如果你只用过 Function Calling你会有一种感觉模型能调用函数但这个能力离“能干活”还很远。函数是零散的每个函数只负责一件小事。比如查库存、生成订单号、发送消息这三个函数之间没有任何关系模型必须在每次对话里重新判断该用哪个。而 Agent 真正工作的场景往往是接收一个任务自己拆解步骤按顺序调用多个函数过程中还要根据返回值调整计划。这就需要一个比函数更上层的抽象技能Skill。技能不是一个函数而是一组能力和流程的组合。它告诉 Agent 的是在什么场景下可以用我、我依赖哪些前置条件、内部按什么顺序执行、什么情况算成功、什么情况算失败。我用一个生活化的例子说明。你去餐厅吃饭点菜这个动作是“函数”但“完成一次用餐”就是技能——它包含找座位、看菜单、点菜、上菜、结账这些步骤。Agent 如果只会点菜它永远无法替你吃完一顿饭但如果它有“用餐”这个技能它就知道整个流程怎么走甚至遇到卖完的菜还会自己换一个。所以agent-skills 本质上是把 Agent 从“能调工具”升级到“能执行任务”的关键中间层。现在很多开源项目和商业产品都在做类似的事情比如把一些常用操作封装成可插拔的 skill 包让 Agent 直接装载使用。1.2 技能与普通工具集的边界在哪很多人会问技能和工具集到底有什么区别我见过不少项目把所有 API 封装成一个个函数然后一股脑塞给模型结果模型经常在调用时迷糊甚至选错参数。这就是因为没有分清边界。工具集的单元是“动作”技能的单元是“目标”。工具集回答“我能做什么”技能回答“我如何完成某件事”。举个实际例子工具get_product_stock(product_id)这是动作。技能check_product_availability(product_query)它内部可能需要先查询商品 ID、再查库存、再查仓库发货状态最后给出一句话结论。技能内部往往要编排多个工具调用并且通常包含模型推理步骤。比如用户说“这个商品明天能到吗”单独的库存查询回答不了这个问题但技能可以先查库存再查物流时效再根据当前时间判断最后返回“明天能到”或“后天才能到”。从工程实现角度看技能应该具备三个特征原子目标、流程意识、失败分支。原子目标指一个技能只完成一件事流程意识指技能内部可以有步骤和状态失败分支指技能在遇到异常时有一个兜底策略而不是抛异常就结束。这也是我在设计技能时最看重的东西。2. 技能定义与设计规范2.1 一个技能文件的完整结构不管你是用代码定义技能还是用 JSON/YAML 元数据描述技能最终都应该包含以下几块内容。我推荐用目录结构来管理一套技能库每个技能一个文件夹里面至少包含一个入口文件和一个描述文件。skills/ product_availability/ skill.yaml run.py inputs.py requirements.txt order_cancel/ skill.yaml run.py inputs.pyskill.yaml是技能给 Agent 看的“说明书”里面包含name: check_product_availability description: 检查商品是否可售包括库存、发货时效、是否支持指定区域配送。当用户询问某个商品是否有货、能不能发到某地时使用。 version: 1.0.0 input_schema: product_query: type: string description: 用户对商品的描述可以是商品名、商品 ID 或链接 region: type: string description: 收货区域例如 华东、华南可省略 optional: true output_schema: status: type: string enum: [available, unavailable, unknown] reason: type: string description: 状态说明 tools_on_chain: - search_product - query_stock - query_shipping_schedule max_steps: 5 timeout_seconds: 15这份描述文件里最关键的是description字段。很多团队不注意这一栏随便写一句“查询商品可用性”结果模型在判断要不要调用技能时经常拿不准。一个合格的 description 应该包含三要素技能是做什么的、什么时候用、什么时候不要用。我甚至会把反例写进去比如“不要在用户只咨询价格时使用”。2.2 输入输出设计怎么让模型容易调用技能设计容易崩的地方有两个入参太灵活、出参太自由。我都踩过。先说入参。给 Agent 用的技能入参一定要贴近“用户原话”而不是贴近“后端接口”。举个例子底层接口要求传warehouse_id和sku_id但用户根本不会说这两个东西。如果你在技能入参里直接暴露这两个 ID模型就得自己先推理出 ID再生成参数这个过程中非常容易出错。正确做法是让技能入参接受product_query然后在技能内部做一次实体解析把用户描述映射成系统内的 ID。这个设计背后的逻辑是模型负责理解意图技能负责执行细节。不要让模型去做它不适合做的事。模型擅长把“红色的那款跑鞋”识别成你要找的商品但不擅长知道这个商品内部 ID 是 10086。所以入参尽量设计成模型容易生成的语义化字段ID 解析放到技能内部处理。再说出参。技能的返回值应该尽量是“结论”而不是“原始数据”。直接返回一段 JSON 原始数据Agent 需要再做一次推理而如果你把数据加工成一句话结论Agent 可以直接引用。比如返回status: available加一句reason: 华东仓有货预计明天送达模型的后续回复就容易很多。当然出参也不能完全没有结构化字段否则无法支持后续逻辑判断。我的建议是结构化状态字段 自然语言摘要两者都要。状态字段给程序用自然语言摘要给模型用。2.3 技能命名与目录管理的几个细节这里分享一些长期积累下来的小规范都不复杂但是能省不少事。命名尽量用“动词 业务对象 场景”的格式。比如query_order_status、modify_shipping_address、calc_cart_total。避免用do_stuff、process_xxx这种毫无语义的名字。原因很简单模型的工具描述窗口有限名字本身也是信息的一部分。一个好名字能让模型少读很多描述。技能目录不要按团队分要按领域分。我见过有些团队把技能放在group_a/、group_b/这种目录下结果技能越多越乱。正确的分法应该是order/、pay/、warehouse/这类按业务域划分因为 Agent 选择技能时也是按业务场景走的。还要注意技能粒度。我见过把“查询商品详情”“查询商品价格”“查询商品评价”做成一个技能的也有拆成三个技能的。这没有绝对标准但有个判断原则技能之间是否经常被同时调用。如果同时调用率超过八成建议合并成一个如果独立调用率很高建议拆分。否则 Agent 要在一个技能内部做很多模型判断反而拖慢速度、增加 token 消耗。3. 从一个真实例子落地让 Agent 学会查库存3.1 准备环境和基础依赖选型这部分我直接说结论。技能运行时和 Agent 主程序可以耦合在一起也可以用独立服务的方式跑我目前倾向于后者——把技能做成独立服务通过 HTTP 暴露给 Agent主程序不关心技能内部实现只关心结果。这样做的好处是隔离性更强。技能内部如果用到了重型依赖比如一个 OCR 模型或者一个库存查询 SDK不会污染 Agent 主环境。而且技能迭代频率高独立部署能避免每次更新都重新发布主程序。准备环境很简单我一般用 Python 3.10装这几个东西pip install fastapi uvicorn pydantic requestsFastAPI 用来暴露技能服务Pydantic 用来做入参校验。如果你是要把技能直接嵌入到现有的 Agent 框架里比如 LangChain、LlamaIndex那就不需要 FastAPI直接按框架的 Tool/Skill 规范写就行。但下面的核心设计逻辑是一样适用的。3.2 实现一个可插拔技能的具体步骤我以一个check_shipping_feasibility技能为例该技能负责回答“这个商品能不能送到我所在地区”。内部逻辑分三步解析商品、查询库存、查询配送范围。第一步定义入参。我设计成接收用户原始描述from pydantic import BaseModel class ShippingFeasibilityInput(BaseModel): product_query: str region_hint: str region_hint是可选字段用户可能直接说了“上海”就传进来没说就留空让技能内部用 IP 或收货地址解析。第二步技能内部编排工具调用。这里我通常会封装一个小的 step 管理器方便处理多步调用from typing import Callable class SimpleStepRunner: def __init__(self, max_steps: int 5): self.max_steps max_steps self.history [] def run(self, step_name: str, fn: Callable, *args, **kwargs): if len(self.history) self.max_steps: raise RuntimeError(step limit exceeded) result fn(*args, **kwargs) self.history.append({step: step_name, result: result}) return result第三步实现主逻辑。内部先调用商品搜索工具得到 sku_id再查库存最后查配送范围。任何一个步骤失败都会抛出带 context 的异常便于外层 Agent 理解原因。def run(self, payload: ShippingFeasibilityInput) - dict: runner SimpleStepRunner(max_steps3) sku_info runner.run(search_product, search_product, payload.product_query) if not sku_info: return {status: unknown, reason: 找不到对应商品} stock runner.run(query_stock, query_stock, sku_info[sku_id]) if stock[available] is False: return {status: unavailable, reason: 该商品当前无货} shipping_ok runner.run(check_region, check_shipping_region, sku_info[sku_id], payload.region_hint) if not shipping_ok: return {status: unavailable, reason: f该商品不支持配送到{payload.region_hint}} return {status: available, reason: 有货且支持配送}这个实现里有几个细节值得说明。每次工具调用的异常我都要求携带阶段信息比如search_product阶段失败和check_region阶段失败对 Agent 来说含义完全不同。如果不携带阶段信息Agent 只能看到“报错了”完全不知道下一步该怎么补救。3.3 把技能接入 Agent 主流程的正确姿势技能服务写好后面向 Agent 的接口我一般只暴露一个统一入口app.post(/skills/check_shipping_feasibility) def skill_endpoint(payload: ShippingFeasibilityInput): return skill_runner.run(payload)然后在 Agent 的配置里注册这个技能描述信息用前面 YAML 里的description字段。不同框架注册方式略有差异但核心原则一致不要让 Agent 拿到技能内部所有细节只给它“什么时候调用 传什么参数”这两类信息。很多团队把技能内部实现细节全量塞给模型比如把SimpleStepRunner的源码贴进 system prompt这完全没必要。模型只需要知道这个技能解决什么问题什么时候调用它入参是什么格式返回结果怎么理解接入之后一定要做一次完整对话测试。我从草稿到稳定通常要跑三轮迭代第一轮看技能能否被正确触发第二轮看出参是否能被模型正确引用第三轮看异常场景下模型能否得体地向用户解释。这三轮都过了这个技能才真正算接进了 Agent。4. 运行中的问题排查与调优4.1 常见故障现象与对策技能上线后最怕的不是“没效果”而是“忽好忽坏”。这里整理几个我实际遇到的问题和排查手段大家可以对照着看。故障现象可能原因排查方向Agent 该用技能的时候没用description 写得太含糊或技能太多太杂精简技能数量优化 description加入“何时不使用”调用了技能但参数传错入参设计得太抽象或字段过多减少必填字段字段改成贴近用户原话的语义化描述技能执行超时内部工具调用串行太多或有外部依赖响应慢增加内部并行调用给外部调用加缓存和超时返回了状态但模型解释错误出参中结构化字段和自然语言摘要不一致确保结构化字段是唯一事实源摘要只是辅助表达技能内部报错Agent 直接摆烂技能异常没有携带阶段信息和修复建议自定义异常类把错误归类并附上可执行的补救提示模型反复调用同一个技能技能出参无法满足 Agent 的判断条件检查出参字段是否能直接覆盖所有分支判断我最常遇到的是第一种和第三种。description 优化这件事我建议花时间写几个真实场景的例子放进 description 里。比如当用户说“这个xx能发北京吗”“xx地区有货没”“我这边能收到吗”时调用本技能。不要在用户只询问价格、图片、评价时调用。这种描述虽然长但模型判断准确率高很多。字长换准确性在技能描述上非常划算。4.2 怎么评估一个技能好坏技能上线不代表结束你需要一个可持续评估的机制。我建议给每个技能定义三个核心指标触发准确率在应被触发的测试集里Agent 有多少次正确触发了该技能。这个指标低问题大多出在 description 命名和技能粒度上。执行成功率技能被调用后内部流程成功跑通的比例。这个指标低说明技能内部实现有问题比如接口不稳定、参数转换不到位。修复有效率技能失败后Agent 或用户根据反馈信息进行二次操作的成功率。这个指标低说明异常信息给得不够。这三个指标我一般做成定时评测任务每个技能积累一批真实历史对话每天跑一遍看波动。如果发现某个技能触发准确率下滑优先怀疑是不是新上线的另一个技能和它发生了描述重叠。关于评估数据有一点要提醒真实数据永远比构造数据靠谱。刚开始没有线上数据时可以用构造数据集但后面一定要替换成真实的用户对话片段否则你会做出一个在测试集上完美、在线上失灵的技能。4.3 技能优化的进阶思路当基础技能跑稳之后有几个方向值得投入。技能间编排。不要只做单个技能可以把几个技能组合成更高阶的技能。比如check_shipping_feasibility和generate_order组合成fast_buy让 Agent 在一次任务里完成“查询下单”。组合时最关键的是确定技能间的数据依赖前一个技能的输出字段要能天然成为后一个技能的入参。技能自描述能力。我最近在给技能加一层自我说明的能力每个技能不仅能完成任务还能在回答用户时解释自己为什么这么做。这对用户的信任感提升很明显。实现上不复杂只要让技能在结果里附带一个trace_summary字段Agent 读取后自然就知道怎么表达。技能的按需加载。当技能数量超过二三十个后全部塞进上下文是非常浪费的。可以在 Agent 前面加一层“技能检索”先把候选技能筛到三到五个再送进模型上下文。检索方式可以用 embedding 相似度也可以基于业务规则。这部分原理类似推荐系统核心是建好技能与场景的索引关系。我个人的体会是agent-skills 这套东西的迭代思路更像在做产品而不是在做算法。你设计一个技能看用户在真实对话里的反馈不断修正描述、修正出参、修正异常处理这才是真正让 Agent 变可靠的方式。指望把模型换一个更大的版本就能自动解决技能调用问题在现阶段不现实。如果你正在设计自己的技能库我建议从最简单的两三个技能开始跑通整个链路先别急着铺量。把描述、执行、异常、评估这四件事都跑顺之后再多技能都只是复制这套方法论而已。
返回列表