ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Agent技能管理实战:从Prompt堆砌到结构化技能编排

Agent技能管理实战:从Prompt堆砌到结构化技能编排 做Agent开发也有小半年了我最大的感受是大多数人不是被模型能力卡住的而是被“技能管理”卡住的。你让Agent做的事越多它的行为就越不可控Prompt越堆越长到最后修一个bug能扯出一串连锁问题。这个项目叫agent-skills核心就一件事把Agent的能力拆成一个个可独立定义、独立注册、独立调用的技能模块让模型在合适的时候选对技能而不是把所有的指令、规则、工具逻辑全部塞进一个巨型System Prompt里。这篇文章我会把这个项目的设计思路、技能定义规范、完整实现过程以及我在实际运行时踩过的坑全部拆开讲。适合正在做Agent应用、或者准备从原型转向工程化的开发者参考哪怕你还没决定用什么框架里面的方法也能直接拿过去用。1. 整体设计思路先想清楚“技能”到底是什么1.1 从Prompt堆砌到技能化一个重要转变先说一个真实场景。早期我做过一个客服问答Agent第一版就是往System Prompt里写各种规则遇到退货问题怎么办、遇到物流问题怎么办、查不到答案怎么回复。刚开始东西少还能撑住后来业务方不断加需求Prompt涨到几千字模型开始出现各种奇怪行为比如把退货规则答到物流咨询里或者明明没有权限却编一个退款方案。后来我把每个业务能力抽出来做成独立技能比如check_order_status、process_return_request、search_knowledge_base每个技能只负责一件事有自己的功能描述、输入参数和返回格式。Agent要做的是根据用户问题判断需要调用哪个技能然后传给技能对应的参数。结构瞬间清晰了模型的行为也稳定了很多。为什么会有这个效果关键在于Prompt是“让模型自己推理该怎么做”而技能机制是“告诉模型有哪些确定的工具和步骤可用”。前者在任务少的时候可行任务一多推理空间变大出错概率也直线上升。后者相当于把每一个能力封装成确定的API模型需要做的从“理解所有规则”简化为“选择正确的技能”。在agent-skills项目中所有技能的统一接口被定义成如下形式每一个技能接收一个结构化参数对象返回标准化的执行结果模型只负责参数填充和技能选择不负责具体步骤的实现。dataclass class Skill: name: str # 技能唯一标识动词开头小写下划线 description: str # 给模型的自然语言描述说明触发场景 parameters: dict # JSON Schema格式的参数规范 handler: Callable # 技能执行函数 def invoke(self, **kwargs): # 统一入口负责校验、执行、结果封装 pass1.2 技能分类不是所有技能都长一个样我在实际拆解技能时发现一个很关键的问题如果把所有功能都平铺开技能一多照样乱。所以agent-skills项目里做了一套分类机制把技能分成四类每类的设计逻辑和调用方式都不同。原子技能只做一件事不依赖其他技能。比如get_current_time、calculate_expression、lookup_order。这类技能最简单参数少结果明确适合作为底座。组合技能内部会调用多个原子技能但对外暴露的是统一接口。比如generate_daily_report这个技能内部可能需要调fetch_sales_data、format_markdown_table、send_message。组合技能的价值在于把固定的执行流程固化下来模型不需要自己规划多步操作。路由技能不直接执行业务逻辑而是根据输入决定调用哪个子技能。这类技能通常用于意图分类比如route_to_human_agent、choose_refund_method。兜底技能又叫Fallback技能当Agent无法匹配任何技能时触发。这是我在第一个版本里忽略掉的设计后来发现没有兜底技能的Agent特别容易“硬答”——模型觉得应该帮用户就开始编造结果。这个分类听起来很基础但它直接影响了后面的注册表设计、测试策略和监控方式。原子技能要多测组合技能要设计好编排顺序路由技能要关注分类准确率兜底技能要不断补充边界场景。1.3 为什么技能化优于传统的Function Calling很多人可能会问现在各个模型厂商都支持Function Calling了直接定义函数不就行了为什么还要做一层技能封装我最初也觉得Function Calling已经够用但实际项目做深了就发现Function Calling只是定义了“模型如何调用函数”的传输协议它没有解决“函数如何组织、如何演进、如何保证稳定”的问题。agent-skills项目把Function Calling当作通信底座但往上增加了几层东西。第一层是技能注册表统一管理所有技能的定义、状态和版本第二层是参数校验与转换层确保模型传进来的参数一定符合预期类型不符合就主动报错而不是带病执行第三层是执行结果标准化层无论技能内部返回什么最终都转换成一个统一格式方便模型解析。这三层加起来才是“技能”和“裸函数”之间的本质区别。打个比方Function Calling相当于给了服务员一本菜单你可以点菜但菜怎么做完全看后厨心情技能的机制则相当于规定了每道菜的标准配方、摆盘方式和出菜时间服务员要做的事情就是在合适的时机推荐合适的菜。对于个人开发者裸函数足够但对于团队协作和长期迭代没有标准化的技能管理会把维护成本拉到不可接受的高度。2. 核心细节技能描述、参数设计与注册机制2.1 技能描述决定模型能否正确调用的第一因素技能描述是给模型看的东西文档是给人看的东西它们的写作逻辑完全不同。我在agent-skills项目里反复打磨了技能描述的规范目前稳定使用的模板包含四个部分技能功能说明、触发条件明确指出什么情况下必须调用、不触发条件明确指出什么情况下不要调用、使用示例。举一个我踩过的真实例子。最开始我写技能描述很喜欢用模糊的句子比如“查询用户订单信息”结果模型经常在用户只问一句“我的东西发货了吗”时不调用技能而是靠训练知识硬答。后来我把描述改成查询用户的订单状态。当用户提到订单、发货、物流、签收、退款进度时必须调用此技能。 参数user_id为用户唯一标识需从会话上下文中获取。 如用户未登录或没有提供user_id不要调用此技能改用ask_for_login技能。改完之后调用准确率从68%左右直接提升到91%。原因很简单这段描述里有明确的触发词、有参数来源说明、还有负面条件。模型不是不会用而是你给的指令不够具体。2.2 参数设计让模型“容易填对”比“功能强大”更重要参数设计的好坏直接决定技能调用的成功率。在agent-skills项目的多次迭代中我得出了几条硬性原则。参数数量越少越好。能用三个参数解决的绝不用五个。每多一个参数模型出错的可能性就多一分。比如订单查询技能我可以拆出user_id、order_id、order_type三个参数后来发现order_type完全可以从订单号推导出来就去掉了。参数命名要语义自明。模型读参数名应该一眼就能明白这个参数该填什么。比如用user_id而不是uid用delivery_address而不是addr。缩写会显著降低模型的理解准确率这一点在大量实测里表现得很明显。给参数设置默认值和约束。比如某个查询技能允许传start_time和end_time但不传时默认查最近7天。约束条件要写清楚日期格式必须是YYYY-MM-DD数量limit取值范围1到100。模型在生成参数时如果看到清晰的约束填对的可能性会大幅提高。2.3 技能注册表管理技能的唯一入口技能注册表在agent-skills项目中承担三个职责注册新技能、更新已有技能、自动生成模型所需的工具定义。早期我把技能定义分散在多个模块里后来发现最大的问题是“模型看到的技能”和“工程里实际存在的技能”不一致删除了一个技能但忘了同步定义模型还在傻乎乎地调用报错率直接拉满。现在我在项目里用一个统一的注册器来管理所有技能代码大致长这样class SkillRegistry: def __init__(self): self._skills {} self._versions {} def register(self, skill: Skill, version: str 1.0): if skill.name in self._skills: raise ValueError(fSkill {skill.name} already registered) self._skills[skill.name] skill self._versions[skill.name] version def unregister(self, skill_name: str): self._skills.pop(skill_name, None) self._versions.pop(skill_name, None) def get_tool_schemas(self): 生成给模型看的工具定义列表 schemas [] for skill in self._skills.values(): schemas.append({ type: function, function: { name: skill.name, description: skill.description, parameters: skill.parameters, } }) return schemas注册表的核心价值是“单一事实来源”模型侧的工具定义、工程侧的代码实现、人看的文档都从这一份注册表生成出来。任何技能变更只改一处其他地方自动同步彻底解决了多头管理导致的分叉问题。2.4 目录结构给项目和团队一个清晰的组织方式一个Agent项目做到一定程度技能数量会快速增长这时候目录结构就不是小事。agent-skills项目采用了按领域分目录、按技能建文件的组织方式skills/ ├── __init__.py ├── registry.py ├── base.py ├── sales/ │ ├── __init__.py │ ├── fetch_orders.py │ ├── calculate_commission.py │ └── generate_daily_report.py ├── customer_service/ │ ├── __init__.py │ ├── check_order_status.py │ ├── process_refund.py │ └── route_to_human.py └── tools/ ├── __init__.py ├── get_current_time.py └── send_message.py这个结构的好处有二。其一按领域组织技能之间相对独立新人接手时能快速定位到自己关心的模块其二每个技能文件独立成模块技能内部的状态和依赖不会互相污染。我在项目里还配合了依赖注入技能不直接引用全局变量需要什么外部依赖都通过构造函数传进来。这样写出来的技能可以单测也可以方便地换成Mock。3. 实操过程从设计到运行的完整实现3.1 定义技能基类统一行为的第一道约束在agent-skills项目中所有技能都继承同一个基类基类负责参数校验、执行调度和结果标准化。基类的设计决定了上层使用的顺畅程度所以我比较早地把这个模块稳定下来后面的代码都在这上面叠加。import json from typing import Any, Callable, Dict from jsonschema import validate, ValidationError class BaseSkill: name: str description: str parameters: Dict[str, Any] {} def __init__(self, handler: Callable): self.handler handler def _validate(self, arguments: Dict[str, Any]): 根据parameters定义的JSON Schema校验输入参数 try: validate(instancearguments, schema{ type: object, properties: self.parameters, required: [ key for key, val in self.parameters.items() if val.get(required, False) ] }) except ValidationError as e: raise ValueError(f参数校验失败: {e.message}) def execute(self, arguments: Dict[str, Any]) - Dict[str, Any]: try: self._validate(arguments) result self.handler(**arguments) return { status: success, data: result, error: None } except ValueError as e: return { status: param_error, data: None, error: str(e) } except Exception as e: return { status: exec_error, data: None, error: str(e) }基类里参数校验用到了JSON Schema这一步极其重要。模型生成的参数经常会有类型问题比如把数字传成字符串、漏掉必填字段。如果没有校验层错误会在技能执行到一半时才暴露排查成本极高。有了统一校验错误能直接在入口处被捕获返回给模型一个“参数错误”的状态模型会自己尝试修正后重新调用。3.2 技能注册示例一个完整的日志分析技能拿日志分析场景来举例。假设Agent收到一条工单“用户反馈支付成功后订单一直显示待付款帮我查一下这个订单在支付网关侧的日志。”在agent-skills项目里我定义了一个技能叫query_payment_gateway_logs负责查出指定订单在支付网关侧的所有日志并提取关键节点。它的参数设计是order_id、trace_id选填用来追踪更精确的链路和time_range选填默认最近24小时。注册代码如下query_logs_skill BaseSkill( namequery_payment_gateway_logs, description( 查询订单在支付网关侧的完整日志用于排查支付退款失败或订单状态不一致问题。 当用户反馈支付成功但订单未更新、退款未到账、或需要对账时必须调用此技能。 order_id必填trace_id有则填没有可省略time_range为ISO8601格式起止时间。 ), parameters{ order_id: {type: string, required: True, description: 订单号如SO20240111001}, trace_id: {type: string, required: False, description: 链路追踪ID}, time_range: { type: object, required: False, properties: { start: {type: string, format: date-time}, end: {type: string, format: date-time}, } } }, handlerfetch_payment_logs ) registry.register(query_logs_skill, version1.2)实际的fetch_payment_logs函数内部会调用日志平台的API把查询结果按时间排序提取出“下单请求”“支付回调”“通知推送”等关键节点最后返回一个结构化结果。这些逻辑都在技能内部完成模型只负责提供参数。效果上模型的调用成功率大概在95%左右剩余5%的失败大多来自time_range格式不符合ISO8601后来我在参数描述里加了格式示例失败率进一步降到了2%。3.3 主控模块如何将技能列表与模型交互打通技能不能只是孤立的函数它们需要嵌入到Agent的主循环里。在主控模块的设计上agent-skills项目采用了一个简洁的循环组装消息历史、附带技能定义、请求模型、判断是否需要调用工具需要就执行技能并把结果追加到上下文然后再次请求模型直到模型输出最终回复。核心片段如下def run_agent(user_input: str, registry: SkillRegistry, messagesNone): messages messages or [] messages.append({role: user, content: user_input}) for _ in range(MAX_TURNS): response llm.chat( messagesmessages, toolsregistry.get_tool_schemas(), tool_choiceauto ) # 模型没有请求工具说明已生成了最终回复 if not response.tool_calls: return response.content # 逐个执行模型请求的工具 for call in response.tool_calls: skill registry.get(call.function.name) if skill is None: messages.append({ role: tool, tool_call_id: call.id, content: json.dumps({error: f未知技能: {call.function.name}}) }) continue result skill.execute(json.loads(call.function.arguments)) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) }) return 已达到最大交互轮数请稍后再试我特意设置了MAX_TURNS循环上限防止模型在多个技能之间来回跳转导致死循环。真实运行中一个复杂任务可能需要调用三四个技能才能完成比如先查订单、再查日志、再查客户信息这时多个轮次的工具调用是正常的但超过6轮还没有结果一般说明模型不知道该干啥了及时终止比继续耗下去更合理。在执行技能时如果返回结果是param_error或exec_error模型会看到错误信息然后自己纠正参数或换一种方式重新调用。这套机制比代码里强制重试更灵活因为模型能理解错误原因而不是盲目的重试。3.4 返回值标准化让模型读得懂是关键技能返回值的设计有一个容易被忽略的点它不仅是一个代码层面的数据结构更是一段会被模型重新理解的文本。所以在agent-skills项目里我把返回值分成三层status表示执行状态data表示业务数据error表示错误信息。模型拿到这个结构后能够快速判断“技能执行成功了吗”“数据里有关键信息吗”这些会直接影响它下一轮的推理判断。另外我在data里保持层级尽量浅一般不超过两层嵌套。太深的嵌套模型容易读丢而且JSON序列化后的字符串会变得很长浪费上下文窗口。比如日志分析技能返回的data结构如下{ order_id: SO20240111001, status: PAID, mismatch: true, checkpoints: [ {step: payment_callback, time: 2024-01-11 14:02:33, ok: true}, {step: order_update, time: 2024-01-11 14:02:35, ok: false} ], summary: 支付网关已收到银行回调但订单系统未更新状态 }我把用于给模型总结的关键信息放在summary字段里这样模型不需要去分析一堆长文本直接读总结就能回答用户。这个做法算是我项目里比较核心的一个经验——技能不仅要执行得对还要把结果“嚼碎了”喂给模型不要让模型自己去大海捞针。4. 常见问题与排查技巧实录4.1 模型不调用技能直接凭记忆回答这是Agent开发里最让人头疼的问题症状是技能定义了请求也发出去了但模型就是不触发工具调用直接给出一个笼统的答案。我第一次遇到时排查了半天最后发现是技能的description写得不够“有吸引力”模型觉得凭自己知识就能回答不需要走工具。这个问题通常有几种解法。第一把触发条件写得非常具体带上用户在对话中最可能使用的词汇比如“支付、退款、订单、物流”这些关键词。第二在System Prompt里加一条规则明确告诉模型“涉及订单状态的问题必须调用技能不得凭记忆回复”。第三就是多轮验证——我发现有些模型版本对工具触发的敏感度不一样同一个技能描述在GPT-4上稳定触发换到另一个模型就经常不触发这时候要根据模型的特性微调描述里的措辞。4.2 技能返回结果太长把上下文窗口塞满了日志查询类的技能最容易犯这个错误。一次查日志可能返回几百条记录直接塞给模型不仅浪费token还会让模型抓不住重点。我在agent-skills项目里做了一个强制约束技能返回的数据总量不能超过一定阈值超过就要在技能内部做摘要、截断或聚合后再返回。具体做法是在技能执行函数里加一个后处理环节比如日志分析技能只返回关键节点的状态和摘要原始明细写到临时存储里如果用户需要详情再通过另一个fetch_log_detail技能获取。这个设计让主上下文的压力大幅减轻也提升了模型对结果的利用率。实际上用户很少需要看全量日志都是需要结论和基于结论的下一步建议。4.3 多技能之间状态冲突A技能改了数据B技能不知道有一次我遇到一个奇怪问题Agent先调用了update_order_address去修改收货地址之后又调用confirm_order去确认订单但确认时系统提示地址不存在。排查后发现两个技能内部都有各自的缓存A技能更新了数据库但没有刷新缓存B技能读取时还是老数据。这不是Agent的调度问题而是技能自身的数据一致性问题。解决方式是约定所有技能在读写同一类业务数据时统一走一个服务层不允许技能内部各自维护缓存。这其实是后端架构的问题但通过技能化的组织方式这类问题反而比传统的单体程序更好定位——每个技能就是一条清晰的依赖链路出问题时顺着链路排查就能找到源头。4.4 排查清单几类必备的调试手段做Agent技能调试我一般会拉起一个离线模式不直接对接线上模型而是用固定的脚本模拟模型调用这样每次的结果都是可复现的。对于技能本身的问题我习惯先跑单测直接调用skill.execute()看返回值是否符合预期。对于模型调度的问题则要看完整的多轮对话日志——不只是最终输出还包括模型每次请求的工具名称和参数。结合agent-skills项目的实际排查经验我整理了一张问题速查表现象常见原因排查方向模型不调用任何技能技能描述不具体或System Prompt没有明确约束检查描述中的触发词添加强制调用规则调用了错误的技能多个技能描述相似模型无法区分增加“不触发条件”让技能边界更清晰参数频繁传错参数命名不直观、缺少约束说明统一命名规范给参数加默认值和格式示例技能执行返回报错技能内部异常或参数校验不过查看错误状态是param_error还是exec_error某技能调用越来越多其他技能入口收窄模型只能选它检查相近技能的描述是否被误改结果正确但模型回答不准确返回数据太乱模型读不懂精简data增加summary字段4.5 实战中我学到的几个技能设计心得最后分享几条我在长期迭代中总结的经验。技能命名要形成一个体系。统一用“动词宾语”的结构比如get_user_info、update_inventory、send_notification。看了名字就知道技能的职责这对模型的理解和代码的维护都有帮助。每个技能最好配一个可执行的“例子”。这个方法我是在一次调试中发现效果特别好的在技能描述里附上一个实际的参数示例模型的填充格式基本不会跑偏。比如get_user_info的参数示例是{user_id: U12345, fields: [name, vip_level]}模型照葫芦画瓢几乎从不出错。一个技能如果经常被修改往往说明它的职责定得不对。有个技能我连续三个版本都在改后来发现因为这个技能承担了“查询用户信息”和“判断用户权益”两件事拆开之后两个技能各自的稳定性马上上去了。技能应该像数据库表一样满足单一职责原则一个技能只做一件事。5. 技能演化与未来扩展从个人项目到团队协作5.1 技能版本管理与线上灰度技能不是写完就一劳永逸的业务变化、模型升级、prompt优化都会推动技能的持续迭代。agent-skills项目里做了一套简单的版本管理每个技能注册时带上版本号线上运行用稳定版本新版本先在测试环境跑通后再手动切换。有些技能的改动可能影响面很大比如改变参数结构或者修改描述文本这种变更最好先在部分流量上灰度。我实际用过一种粗暴但有效的方法注册两个同名技能一个版本号是1.0一个是1.1-beta通过配置中心决定走哪个版本验证没问题后把1.0替换掉。虽然方法土但胜在直观、可控。等到你需要维护几十个技能时一套统一的版本策略会帮你省掉大量线上事故。5.2 从个人开发到团队协作的技能治理当Agent项目从小规模原型走向团队协作时技能管理会面临新的挑战。首先是命名冲突两个人同时加技能很可能都起了get_order_info这个名字其次是职责重叠A技能和B技能做的事情有六成相似但各自参数略有不同新成员根本不知道该用哪个。解决这个问题不能靠自觉要靠流程和约束。在项目里技能注册表是唯一入口任何新增、修改、删除都必须经过注册表注册表里可以加一个简单的审核日志记录是谁、在什么时间、改了什么技能、为什么改。这能给团队协作提供可追踪的记录避免技能库越维护越混乱。至于职责重叠建议在新增技能前先在现有技能列表里搜一遍如果能复用现有技能就不新增如果确实要新增需要在技能描述里写明与现有技能的差异点。5.3 我的下一步打算和持续优化的思路agent-skills项目目前已经稳定支撑了几个内部Agent应用接下来我打算把重心放在三件事上一是技能自动化测试维护一个覆盖所有技能的测试集每次变更后自动跑一遍确保没有回归二是技能效果评估定期抽样分析线上对话检查技能调用是否准确、参数填充是否合理三是技能热更新目前技能变更还需要重新部署服务我打算把它变成动态加载的形式在不重启的情况下就能注册、替换、下线技能。做Agent和做传统后端有一个完全不同的核心差异传统后端确定性高功能跑通一次就基本稳定Agent天然带有不确定性同一个技能同一套参数不同模型的调用结果可能天差地别。这一点让技能设计里优秀的工程规范变得更重要。我在这段时间里的核心体会是不要过度依赖模型的能力去弥补技能定义的混乱结构清晰永远比调参和堆Prompt更可靠。后续如果你也在做Agent技能方向建议从找一个小场景开始定义三到五个技能跑通闭环把流程理顺再慢慢扩充你会发现比一上来就铺一大片“看似全能”的功能要稳得多。
返回列表