这两天一直在折腾 Agent 相关的项目,正好把"技能"这块的内容重新梳理了一遍。所谓 agent-skills,说人话就是:你给大模型装上一双"手",让它不只是会聊天,还能真正去操作点什么。今天我把自己的实践经验和踩坑记录整理出来,从技能的设计思路、实现方式到调试技巧,一次性聊透。
什么是 Agent 技能?简单讲,就是一组能被大模型自主调用的能力模块。你可以把大模型想象成一个刚入职的实习生,脑子好使、理解力强,但什么工具都不会用。技能体系就是给这个实习生配齐办公软件、数据库权限、API 接口,并告诉他"遇到什么情况该用哪个工具"。没有技能的 Agent 只能纸上谈兵,有了技能它才能真的干活。
下面我会以一个真实项目为例,拆解技能的完整落地过程,包括框架选型、技能定义、代码实现、测试调优,以及几个我踩过的大坑。内容偏实操,想直接上手的朋友可以照着敲。
1. 技能化改造:为什么说这是 Agent 落地绕不开的一步
1.1 大模型只是"大脑",技能才是"手脚"
早期做 AI 应用,大家喜欢把业务逻辑全部塞进 Prompt 里,让模型自己发挥。这种方式在聊天场景还凑合,但到了真实业务里就崩了:模型不知道当前库存数据、没法调外部接口、算不对复杂账目、甚至会把单位搞混。问题不在模型笨,而是它没有"工具"。
Agent 技能本质上是把模型的能力边界向外扩展。我在项目里最开始用的方案是直接在 System Prompt 里写几十条规则,告诉模型"你能做这个、能做那个"。结果 Prompt 越长,模型越容易懵,经常答非所问,还总幻觉出不存在的数据。后来换成了技能化方案:把每个能力封装成独立的工具函数,用 JSON Schema 描述参数,让模型按需调用。效果立竿见影,模型不再瞎猜,而是明确知道"哦,这里我需要调 get_stock_price 这个技能"。
所以我的第一个建议是:如果你的 Agent 还停留在"纯 Prompt 对话"的阶段,那它距离"可用"还有很长的路。技能化改造是必经之路,越早做越好。
1.2 技能化拆解带来的三个实际好处
我在实际项目中体会到的技能化收益,总结下来就三点。
第一,可测试性大幅提升。每个技能函数都是独立单元,可以单独写测试用例,出问题能快速定位是技能本身 bug 还是模型调度错了。以前那种"全塞 Prompt"的方式,出问题你根本不知道是哪里坏了。
第二,代码复用变得自然。多个场景共用的能力(比如查天气、算运费、发通知)抽成技能后,任何业务场景的 Agent 都能直接挂载,不用重复开发。我们团队后来新起一个项目,复用已有技能模块节省了大约三分之一的工作量。
第三,升级迭代的路径清晰了。每次给 Agent 加新能力,本质上就是写好一个新技能函数 + 定义好描述 + 注册进技能列表。这套流程标准化之后,团队里的新同学也能快速上手。
2. 技能体系设计:从需求分析到模块划分的完整思路
2.1 先梳理业务场景,而不是先写代码
我很早之前犯过一个错:一上来就开始写技能函数,写完之后才发现技能列表东一个西一个,有的技能之间功能重叠,有的技能又没人用。后来我老老实实先做了业务场景梳理,效果完全不一样。
我用的方法是画场景矩阵。把用户可能会提的需求全部列出来,然后按"频率"和"复杂度"两个维度分类:
- 高频且低复杂度:比如查订单状态、算价格,这类技能优先做,ROI 最高。
- 低频但高复杂度:比如生成季度报表,这类可以做成"复合技能",内部编排多个基础技能完成。
- 低频且低复杂度:暂时不做,让模型直接回答或引导用户走人工流程。
- 高频但高复杂度:这类是难点,需要拆成多个细粒度技能,再加一层调度逻辑。
我自己实际项目里要处理的场景是"企业内部的智能助理",用户会问库存、销售额、物流状态、售后政策等等。按上面的矩阵一梳理,最终确定首批做 8 个基础技能、2 个复合技能。先跑通核心闭环,再逐步补充。
2.2 技能粒度怎么定:粗了不管用,细了管不过来
技能粒度的把控是设计阶段最考究的一个点。我刚开始做的时候把技能切得非常碎,比如"获取用户姓名""获取用户手机号""获取用户地址"分开做成了三个技能。结果模型调度的时候频繁出错,经常需要连续调用三次才能凑齐一个用户信息。
后来又走另一个极端,把"获取全部用户数据"做成一整个大技能,参数列表长得吓人。这种设计的问题也不小:一方面模型容易传错参数,另一方面某些场景只需要一个字段也把全部数据拉回来,白白浪费 token。
最终我找到比较合理的平衡点是:按"业务操作"而非"数据字段"来划分技能。比如对用户信息这个场景,做一个 get_user_profile 技能,内部返回结构化完整对象,但允许模型只取其中部分字段来用。这样既不会太碎,也没有过度聚合。
粒度判断的统一标准,我总结了三句话:
- 一次技能调用对应一个完整的业务动作(而不是一个数据查询);
- 技能参数控制在 3~5 个以内,太多说明粒度偏大或边界不清;
- 如果多个技能内部调用了同一段核心逻辑,那这段核心逻辑应该抽成公共组件,而不是做进多个并列技能。
2.3 技能间的依赖与冲突:避免"一个技能调另一个技能"
实际操作中我强烈建议:技能之间不要互相调用。先别急着反对,我解释下原因。技能互相调用会让系统的调试难度直线上升。假设技能 A 内部调了技能 B,B 内部又调了 C,一旦 C 出问题,你排查的时候根本不知道是哪一层出了问题。更重要的是,大模型调度技能本身就存在不确定性,技能嵌套调用等于在这个不确定的链条上又加了一层不确定。
正确的做法是抽象出一层公共逻辑层。如果两个技能都需要用某个公共能力,那就把这个能力写成一个普通函数放在 utils 里,两个技能直接调用它,而不是技能之间互相"对话"。这个设计原则让我在生产环境省了太多心,强烈推荐。
2.4 技能描述的写法:模型的"使用说明书"
技能函数名、参数 Schema 之外,还有一个容易被低估的部分:技能描述(description)。模型靠什么判断该不该调用这个技能?主要就看描述。我见过太多人草草写一句"获取订单信息"就完事了,结果模型该调用的不调用,不该调用的一通乱调。
好的技能描述应该包含:这个技能是干什么的、适用场景是什么、有哪些边界或限制、调用后返回什么。我举个例子,"get_order_info"这个技能,我最终定下来的描述是:"根据订单ID查询订单详情,包括商品列表、金额、支付状态、物流单号。用于用户询问'我的订单怎么样了''发货了没'等场景。如果用户没提供订单号,先调用 get_order_list 获取订单列表。注意:本技能只支持已支付的订单,预售订单查不到物流信息。" 写完之后,模型决策的准确率肉眼可见地提高了。
3. 实操过程:从零实现一套 Agent 技能系统
3.1 框架选型:为什么我选择了开源工具链而不是自研
技能系统说白了有两件事要做:一是把技能函数注册成模型能理解的格式,二是把模型输出的"调用意图"解析成真实代码执行。这两件事没必要重复造轮子,目前开源生态里已有比较成熟的方案。
我做选型的时候对比过几套方案:
- 基于 LangChain 的 Tool 机制:生态最丰富,网上资料多,团队上手快。
- 基于 OpenAI Function Calling 原生的函数定义方式:最轻量,适合没有复杂链路、直接调用模型接口的场景。
- 基于 Semantic Kernel 的 Plugins 方案:如果你在 .NET 技术栈,这个可以看。
- 自己写一套"描述 + 函数映射"的轻量框架:如果你们的技能数很少(少于 5 个),这种方式也可以,够用就行。
我实际项目选的是 LangChain + 原生 Function Calling 混用的方案。为什么这么选?因为 LangChain 的抽象层级比较完善,把模型的对话管理、上下文记忆、工具回调都封装好了;而原生 Function Calling 在函数定义格式上更标准,两者结合既能快速开发,又不至于被框架绑死。这里提醒一句,框架只是工具,真正核心的其实是你的技能设计做得怎么样。工具选得再好,技能本身定义得稀烂,模型照样给你乱调。
3.2 技能注册与实现:一份可落地的代码示例
下面这段代码我从项目里简化出来的,展示了一个技能的全过程。技术栈是 Python + LangChain + OpenAI 兼容接口。
首先要定义一个技能函数,函数本体执行真实的业务逻辑。比如查询库存:
# skills/inventory.py import json from typing import Optional def get_stock_level(sku: str, warehouse_id: Optional[str] = None) -> dict: """查询商品库存量。 Args: sku: 商品编码,必填。 warehouse_id: 仓库ID,可不填,默认查全部仓库。 Returns: dict: 库存信息,包含 sku、总库存、各仓库明细。 """ # 这里替换成你的真实数据源 fake_db = { "SKU-1001": {"total": 328, "warehouses": {"WH-01": 180, "WH-02": 148}}, "SKU-1002": {"total": 56, "warehouses": {"WH-01": 30, "WH-02": 26}}, } if sku not in fake_db: return {"error": f"sku {sku} 不存在"} data = fake_db[sku] if warehouse_id: if warehouse_id not in data["warehouses"]: return {"error": f"仓库 {warehouse_id} 无此商品"} return {"sku": sku, "warehouse_id": warehouse_id, "stock": data["warehouses"][warehouse_id]} return {"sku": sku, "total": data["total"], "warehouse_detail": data["warehouses"]}然后你需要把这个函数"翻译"成大模型能读懂的格式。以 OpenAI Function Calling 的格式为例:
# skills/registry.py get_stock_level_schema = { "type": "function", "function": { "name": "get_stock_level", "description": "根据 SKU 查询商品库存。当用户询问'还有货吗''库存多少'时使用。" "支持按仓库查询,不传仓库ID则返回全仓汇总。", "parameters": { "type": "object", "properties": { "sku": { "type": "string", "description": "商品编码,格式如 SKU-1001" }, "warehouse_id": { "type": "string", "description": "仓库ID,可选,格式如 WH-01" } }, "required": ["sku"] } } } SKILL_REGISTRY = { "get_stock_level": get_stock_level, # 其他技能... }第三步是搭一个执行循环,让模型在对话中自主决定是否调用技能以及调用哪个。核心逻辑用伪代码表达:
# agent_loop.py from skills.registry import SKILL_REGISTRY, get_stock_level_schema import json def run_agent(user_input: str, messages: list): llm = get_llm() # 你的模型实例 # 把技能格式传给模型 tools = [get_stock_level_schema] messages.append({"role": "user", "content": user_input}) response = llm.chat.completions.create( model="your-model", messages=messages, tools=tools, tool_choice="auto", ) # 如果模型决定调用技能 while response.choices[0].message.tool_calls: msg = response.choices[0].message messages.append(msg) for call in msg.tool_calls: func_name = call.function.name arguments = json.loads(call.function.arguments) result = SKILL_REGISTRY[func_name](**arguments) messages.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False) }) response = llm.chat.completions.create( model="your-model", messages=messages, tools=tools, tool_choice="auto", ) # 最终模型根据技能返回结果,生成给用户的自然语言回答 return response.choices[0].message.content这段逻辑你一定要亲手写一遍,不要只停留在"看过"。它把 Agent 技能的完整闭环串了起来:模型识别意图 -> 选择技能 -> 填入参数 -> 执行函数 -> 把结果反馈给模型 -> 模型组织自然语言回答。整个链路理解了,Agent 技能你已经掌握大部分了。
3.3 一个复合技能的设计:内部编排多步操作
前面提到复合技能,这类技能本身也是一个函数,但它内部会依次调用多个基础技能或外部服务。举例,我在项目里做过一个"订单全链路追踪"的复合技能,用户只需要说"我的订单到哪了",这个技能会自动完成:调订单接口拿订单详情 -> 判定订单状态 -> 如果已发货则调物流接口 -> 汇总状态输出。模型只需要做一次"调用决策",复杂的编排逻辑隐藏在技能内部。
为什么要这么设计?核心原因是减少模型的决策负担和调用次数。每次模型决策都伴随不确定性,调用次数越少,出错概率越小,token 开销也越低。如果你的复合技能内部逻辑稳定不变,完全可以在代码层面写死,没必要让模型分步执行。
# skills/order_tracker.py def track_order_full(order_id: str) -> dict: """复合技能:追踪订单全链路状态。""" # 第一步:查订单详情 order_info = get_order_detail(order_id) if not order_info or order_info["status"] == "pending": return {"order_id": order_id, "status": "未发货", "timeline": []} # 第二步:已发货则查物流 shipment_id = order_info["shipment_id"] logistics = get_logistics_track(shipment_id) latest_status = logistics["tracks"][-1]["desc"] if logistics["tracks"] else "暂无轨迹" return { "order_id": order_id, "status": order_info["status"], "latest_logistics": latest_status, "logistics_list": logistics["tracks"] }复合技能的下层基础技能同样需要注册到技能列表吗?不需要。下层技能是普通函数即可,只暴露复合技能给模型调用。这样技能列表保持精简,模型的决策空间也更干净。
3.4 参数校验与自动纠错:模型传参不可靠,必须有兜底
作为一个踩坑无数的人,我必须强调:模型传参真的不可靠。哪怕你 Schema 定义得再严格,模型有时候就是会给你传一个不存在的时间格式、漏传必填字段、甚至把字符串和数字类型搞混。所以技能函数内部一定要做防御式编程。我项目里类似 get_stock_level 这样的函数,开头必然有一段参数校验:
def get_stock_level(sku: str, warehouse_id: Optional[str] = None): sku = (sku or "").strip().upper() if not sku.startswith("SKU-"): # 尝试自动补全 sku = "SKU-" + sku if warehouse_id: warehouse_id = warehouse_id.strip().upper() # 后续逻辑...这个"自动纠偏"的思路很重要。你与其让模型报错重来,不如在技能内部做一层容错。因为一次错误调用不仅浪费一轮对话,还可能让用户觉得 Agent 很蠢。
3.5 技能测试:单测、集成测试、回归测试三层保障
技能代码写完之后,直接上线?我劝你冷静。技能系统的测试要分三层跑:
第一层是单元测试,针对技能函数本身。用 pytest 之类的工具,把正常输入、异常输入、边界输入全跑一遍。第二层是模型调度测试,模拟用户指令,看模型选技能选得对不对、参数填得对不对。这一层是技能系统最特殊的地方,因为模型有随机性,同一句话多跑几次结果可能不同。我做的时候会固定随机种子,同时每条用例跑 5 遍,要求至少 4 遍通过算合格。第三层是端到端回归测试,把真实用户对话历史回放,看改造技能后有没有影响其他现有功能。这个我建议接入 CI/CD,每次改技能描述或参数都自动跑一遍。
下表是我在日常测试的时候常用的用例示例,大家可以参考一下:
| 用例类型 | 用户输入示例 | 期望触发技能 | 期望参数 |
|---|---|---|---|
| 正常场景 | B1001 还剩多少库存 | get_stock_level | {"sku": "SKU-1001"} |
| 参数缺失 | 帮我看看库存 | get_stock_level(引导补充) | {}(模型应主动追问) |
| 边界场景 | 库存调一下 | 无对应技能,拒绝调用 | - |
| 多技能场景 | 这个订单发货没,顺便看下库存 | track_order_full | {"order_id": "..."} |
4. 常见问题与排查技巧实录
4.1 模型死活不调用技能,怎么办
这是群里被问得最多的问题。症状是用户问了跟技能强相关的问题,模型却直接凭记忆编了一个答案。排查路径我建议按顺序来:
检查技能是否真的传给模型了。很多人改完代码,tools 列表没更新,模型自然不知道新技能。这个低级错误我犯过不止一次。检查技能描述是否和用户的表达方式匹配。模型判断是否调用技能,靠的核心就是描述文本。你写"查询库存量",用户说的是"还有货吗",模型可能就反应不过来。可以把描述改成"查询库存量,用于回答用户关于商品是否有货、剩余多少的问题"。检查模型的 system prompt 里有没有暗示它"直接回答"的语句。
4.2 技能调用了,但参数填得乱七八糟,怎么治
我遇到过一个具体案例:get_stock_level 技能明明定义好了参数格式,模型却喜欢把 "SKU-1001" 传成 "sku1001",导致每次查询都报错。后来我在技能描述里明确了"SKU 格式为 SKU-四位数字,用户只提供数字时请自动补全 SKU- 前缀",问题立刻缓解。
另外,参数描述里可以给出常见的错误示例,模型会更清楚边界在哪里。比如在描述里写上"如果用户给出的是商品名称而非 SKU,不要调用本技能,应调用 search_product_by_name"。这个技巧很实用。
4.3 一次对话里模型连续调用同一技能多次,浪费 token 还慢
这也是我遇到过的问题。用户问"这双鞋和那双鞋的库存分别是多少",模型分成两次调用 get_stock_level,而不是一次数组参数搞掂。这种情况如果要根治,需要把技能设计成支持批量查询。如果不方便改接口,另一个做法是在技能描述里写明"如需查询多个商品库存,请分别调用本技能多次"。这是目前我找到的最省事方案。
4.4 我在技能系统迭代过程中沉淀的几条心得
最后分享几点我在踩了无数坑之后才想明白的事。
技能描述不要写得像 API 文档,要写得像写给人类同事的便签。你平时怎么告诉同事"这个接口怎么用、注意什么",就怎么写描述。模型对自然语言的接受能力远超结构化字段。
技能数量和调用成功率不是线性关系。技能越多,模型的"选择困难症"越明显。我实测下来,单一模型一次对话中暴露的技能数最好控制在 8~12 个以内。超过这个量,模型开始频繁选错或犹豫不决。解决方案是把技能分组,根据用户意图先匹配合适的技能组,再在组内做具体选择。
技能的执行结果一定要加错误信息。这样即使用户提供的参数有问题,模型拿到"工具报错"的结果后,也能自动调整策略重新获取正确参数。我就靠着这个机制,让很多原本需要人工介入的错误被模型自己纠正了。
另外还要提一句,在技能代码里做好运行日志是投资回报率极高的习惯。每次技能调用记录一下输入输出,用简单的 JSON 格式打在本地日志里,线上出问题能快速复盘。我见过太多人上线了技能却不打日志,出了问题只能拍脑袋猜。
这套技能体系搭好之后,之后再接新的业务场景其实就变成"填空题"了:梳理需求、写技能函数、定义描述、注册、测试。流程顺手之后,一个中等复杂度的技能大概半天就能交付。这篇内容算是我做 Agent 技能系统迄今比较完整的一次复盘,希望里面提到的思路和坑能帮你少走一段弯路。