ARTICLE DETAIL

资讯详情

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

Agent技能化实战:从Prompt过载到动态路由注入

Agent技能化实战:从Prompt过载到动态路由注入 1. 从失控的 Prompt 工程聊起为什么 Agent 需要“技能”过去一年多我一直在做 LLM Agent 相关的应用落地从最初的 Demo 到后面真的要跑业务最大的感受就是Prompt 工程会越来越痛苦而“技能化”是唯一的出路。我第一次把 Agent 推向生产环境时系统里有搜索、查数据库、发邮件、写周报、定时提醒五个功能。当时我没想太多直接把所有功能的说明、参数、注意事项全写进 system prompt模型也确实能用。但等到第八个、第九个功能加上去之后问题开始变得不可控明明用户问一句“帮我看看昨天的销售数据”模型却开始纠结“用户是不是想让我写邮件” 上下文一长它甚至会把查询参数拼错把日期字段传给客户名称字段。这就是典型的prompt 空间过载。你让一个模型在同一时间“知道”所有能力的存在它就很难在所有能力之间做精准路由。就像一个人脑子里同时装着几十份岗位说明书真到干活的时候反而不知道该执行哪一个。所以后来我把整个架构推倒重来换成了“agent-skills”的思路每个能力单独封装为独立技能需要时才把对应技能描述注入上下文。用的时候模型只看到和当前任务相关的几个文档块而不是一锅炖的全部说明。这个改动之后我刚才说的那些低级错误几乎消失推理时的 token 消耗也降了一大截。这篇文章就是想把这个踩坑过程、设计思路和落地细节完整写出来。对你来说不管你是正在做 Agent 应用还是准备把现有的工具链改造成 Agent 能调用的形态这套方法论都能直接抄。1.1 “技能”不是工具调用的新马甲这里必须先划分清楚一个概念边界Agent Skills 和 Function Calling / Tool 不是一回事虽然很多人把它们混着叫。Function Calling 是模型根据函数声明生成结构化的参数然后由你的代码去执行。Tool 通常是指外部 API、函数、插件的封装。Skills 是一种更高层、更完整的“能力包”它除了描述“这个能力怎么调用”之外还负责“这个能力什么时候适用”“它需要前置条件吗”“执行完之后返回什么状态”这类决策信息。拿生活中类比Tool 相当于一把螺丝刀Skills 相当于“维修”这个完整的技能——螺丝刀、操作规范、什么时候用、怎么判断修好了都在里面。这个区别直接决定了实现方式上的差异。如果一个功能只是“输入某 ID 返回某信息”那它就是工具。但如果一个功能需要你判断用户意图、选择参数、完成多个步骤、还涉及错误恢复那才配叫 Skill。1.2 一个能落地的 Skills 系统至少要回答四个问题我在设计 agent-skills 框架时强制自己围绕四个问题展开后续所有代码和模块都是这四个问题的延伸我有哪些技能当前这个任务该用哪个技能怎么调用这个技能参数怎么填技能执行结果怎么回到模型主循环从而继续推进对话这四个问题看起来基础但其实很多失败项目都是因为在第一、第二个问题上没想清楚导致系统能力很丰富模型却不知道什么时候该用哪个。2. 把“技能”拆开看一个 Skill 单元到底该包含什么我在自研这套轻量级 Skills 框架的时候没有直接套用某些重量级 Agent 框架而是自己定义了一套极简结构。每个 Skill 单元包含四层声明层谁来发现我、描述层我适合干嘛、执行层我具体怎么做、反馈层我做完之后的结果长什么样。本项目为agent-skills实战示例2.1 声明层技能的身份信息class SkillBase: name: str # 技能唯一标识小写字母下划线如 query_sales description: str # 一句话描述用于模型路由决策 parameters: dict # JSON Schema 风格参数定义 required_permissions: list # 权限声明如 [db_read, email_send]这里最核心的是description我把它形容为技能的“销售文案”。模型并不会读你的源码它只能通过描述文字来判断“这个技能对我当前任务有没有用”。描述写得太笼统模型不敢用写得太狭窄模型该用的时候也想不到。比如一个搜索技能的描述一开始我写的是“搜索信息”模型接得稀烂。我改成“Search the internal knowledge base for company policies, benefits, and HR procedures. Use this when user asks about rules or process documents”命中率立刻上来了。关键就是写清楚能力域和触发信号。2.2 逻辑层技能的三种主流实现形态不需要过度设计就三种形态适用场景实现成本灵活度LLM 指令型技能内部仍然依赖大模型推理低高确定性代码型有明确规则和计算逻辑中中外部 API 型需要访问外部系统中低我现在的建议是能用代码就用代码只有跟语言理解强相关的时候才在技能内部再调一次 LLM。比如“提取周报关键信息”这个技能直接让主 Agent 做是一层但如果你想把这个能力输出为稳定结构那就包一层独立 LLM 调用输入原文、输出结构化字段。这样的好处是可以单独对技能做测试和优化不影响主 Agent 的路由逻辑。2.3 描述层的具体写作规范这一节是我实操中总结出来的价值最高我把它列成清单描述第一句必须是“能做什么”不要写废话。第二段写“什么时候用”给出 2-3 个典型触发场景示例。如果有明确的反例也写上去。例如“当用户询问天气时不要使用本技能”。参数说明里每个字段都要写清楚取值范围和单位模型才能填对。如果技能有副作用比如发邮件、删除数据必须在描述中显著标注。比如一个合格的发邮件技能描述大概是Sends an email to internal team members. Use when user asks to notify, inform, or follow up with a colleague. Requires recipient email or name, subject, and body. This is a WRITE action and cannot be undone, confirm with user before execution.这里最后一句话很关键它给了模型一个内部的“安全阀”让它在执行前主动向用户确认。3. 落一套可运行的技能框架注册、路由与注入理论讲完直接上代码。下面是我在实际项目中验证过的一套极简实现你们可以直接拿去改。核心就是三个模块注册中心、路由选择器、动态注入器。3.1 技能注册中心# skill_registry.py from typing import Dict, Type from skill_base import SkillBase class SkillRegistry: _skills: Dict[str, SkillBase] {} classmethod def register(cls, skill: SkillBase): 注册一个新技能到全局表 if skill.name in cls._skills: raise ValueError(fSkill name conflict: {skill.name}) cls._skills[skill.name] skill return skill classmethod def get_all_descriptors(cls) - str: 拼接所有技能的描述文本用于动态注入 blocks [] for skill in cls._skills.values(): blocks.append(f## Skill: {skill.name}\n{skill.description}\n) return \n.join(blocks) classmethod def get(cls, name: str) - SkillBase: if name not in cls._skills: raise KeyError(fSkill not found: {name}) return cls._skills[name]注册中心要解决的是“技能冲突”。早期我犯过一个错两个技能里都写了“查数据”这个关键词模型路由时就疯了不知道该选哪个。后来我加了命名规范并让新注册技能必须通过语义去重检查才算解决。注意技能名一旦注册尽量不要临场改名改名的成本远比你想象中高。模型如果已经在上下文中学到旧名字新的名字需要很久才能建立映射。3.2 路由选择器让模型只看到匹配的技能路由是 agent-skills 的灵魂。路由质量直接决定最终效果。我尝试过两种方案方案 A全量注入把所有技能的描述一次性拼进 system prompt。优点是最简单缺点是上下文被垃圾描述占据、模型决策变慢、token 成本高。我第一个版本就是它也正是它逼我做了路由。方案 B基于语义相似度的预筛用户请求进来之后先用一个轻量文本 Embedding 模型算向量跟每个技能的描述向量算相似度取 Top-K只把 Top-K 的技能描述注入系统提示词。# router.py import numpy as np from sentence_transformers import SentenceTransformer class SkillRouter: def __init__(self, registry): self.registry registry self.encoder SentenceTransformer(sentence-transformers/all-MiniLM-L6-v2) self.skill_vectors { name: self.encoder.encode(skill.description) for name, skill in registry._skills.items() } def route(self, user_query: str, top_k: int 3) - list: q_vec self.encoder.encode(user_query) scores { name: np.dot(q_vec, skill_vec) / (np.linalg.norm(q_vec) * np.linalg.norm(skill_vec)) for name, skill_vec in self.skill_vectors.items() } sorted_names sorted(scores, keyscores.get, reverseTrue) return [name for name in sorted_names[:top_k] if scores[name] 0.3]方案 B 上线后的效果变化非常明显单次请求注入的描述文本从几千 token 降到几百 token模型选错技能的概率也显著下降。这个 0.3 的阈值是基于我自己的数据集调出来的你们落地时一定要根据自己的业务重调。3.3 动态注入主循环里怎么接技能调用动态注入包含两种时机启动阶段把系统级技能例如“思考步骤”“输出格式化”注入。每轮对话前根据当前用户消息做路由把命中的技能描述追加进上下文。async def agent_loop(user_message, session): # 1. 路由选中候选技能 candidates router.route(user_message, top_k3) # 2. 注入描述到 system prompt descs \n.join([registry.get(name).description for name in candidates]) system_prompt fYou are a helpful agent.\nAvailable skills:\n{descs} # 3. 调 LLM 生成响应模型可能返回调用指令 response await llm.chat(session [{role: user, content: user_message}]) # 4. 如果响应中带技能调用指令则执行对应技能 if response.skill_call: skill registry.get(response.skill_call.name) result await skill.execute(**response.skill_call.arguments) # 5. 把执行结果拼回历史再让模型生成最终回答 return await llm.chat(session [{role: assistant, content: response.text}, {role: tool, content: json.dumps(result)}]) return response.text这套循环本质上是选技能 → 传参数 → 执行 → 回填结果 → 再生成。每一步之间的数据结构要稳定否则模型很快会迷路。3.4 为什么我不直接套用现成 Agent 框架期间我试过 LangChain、AutoGPT 等框架的 skills 模块也试过国内外几个商业平台的 skill 体系。最终结论是框架太重约束太死与业务的耦合度太高。我希望技能可以从“某个 Agent 实例”中剥离出来独立注册、独立测试、可以被多个 Agent 复用。而大而全的框架往往把技能绑定在特定的 Agent 类或工作流上迁移一次成本极高。所以我保留了极简骨架只做四件事注册、路由、注入、执行。剩下的一切由业务侧自己扩展。这个选择让我后续维护成本低了很多。4. 失误复盘技能不生效的五个根源框架搭起来了不等于万事大吉。在实际调优阶段我反复遇到“技能存在但模型就是不调用它”的问题。以下是我统计出的五个高频根因每一个都是真实踩坑出来的。4.1 技能描述与用户口头表达之间缺乏桥接不少技能描述用的是“系统视角”而不是“用户视角”。例如一个查询订单状态的技能描述写的是“Query order status by order ID”但用户真正的说法是“我的包裹到哪儿了”“东西发了没”。这两者之间缺少触发词桥接。修复建议把真实用户的几种说法整理出来直接揉进描述里让模型能建立链接。比如加上“Use when the user asks about delivery, package, shipment, arrival, or order progress.”4.2 候选技能过多导致决策不聚焦一开始 Top-K 我设置为 5结果模型反而开始犹豫甚至在多个相似技能之间反复横跳。后来我测试对比发现候选技能控制在 2~3 个准确率最高。这背后的逻辑很直接大模型是概率模型选项越多决策熵越高。把 Top-K 缩小相当于帮它减负。4.3 参数 Schema 约束过死模型无法理解业务取值比如日期参数我最初定义为date: {type: string, description: the date}。模型经常把“这周”“去年这个时候”翻译得乱七八糟。后来我改成了date: {type: string, description: target date in YYYY-MM-DD. If the user says relative date like yesterday or last week, first resolve the actual date before calling.}。模型一通操作之后参数准确率直线上升。核心教训模型天生不会“帮你想”它只会按你描述给的规则做事。你描述得越具体它执行得越准确。4.4 技能的返回结果没有给模型提供“可读性”技能执行完之后返回的数据有些是 JSON 存储的原始记录字段名晦涩难懂。模型拿到这种结果后很难生成好的自然语言回复。解决办法是让每个技能在返回结果里附上一段summary字段。执行逻辑内部把关键信息提取出来压缩成一段人类可读的话模型直接基于 summary 生成答案就不会胡说。# skill_return.py class SkillResult: success: bool data: dict # 原始数据供程序消费 summary: str # 人类可读总结供 LLM 消费 error: str None4.5 忽略技能执行后的状态校验很多技能不是一次性返回的它的执行结果是改变系统状态比如创建工单、发送通知。这类“有副作用”的技能很多项目根本没做状态校验。我的做法是写操作类技能执行后主动去查一次结果确认成功了summary 里写“已成功执行通知 ID 为 xxx”如果失败summary 里写“执行失败原因xxx”。这一步看起来多了一次查询但避免了一大批“模型以为发送了邮件但实际没发”的连环事故。5. 让技能可测可观测单元测试到真机调优Agent 类项目的最大痛点就是“不稳定性”同一个技能今天可用、明天不可用今天命中、明天偏离。为了稳我把 skill 单独当成软件模块去测而不是每次测一整个 Agent。5.1 给每个技能造一套独立测试集# tests/test_query_sales.py CASES [ {query: 昨天华东区销售多少, expected_skill: query_sales, expected_param: {region: 华东, date: 2025-01-14}}, {query: 帮我拉一下北京上周的手机销量, expected_skill: query_sales, expected_param: {region: 北京, product: 手机, date: 2025W02}}, {query: 今天天气怎么样, expected_skill: weather, expected_param: {}}, ] def test_router_accuracy(): for case in CASES: routed_name router.route(case[query])[0] assert routed_name case[expected_skill], fExpected {case[expected_skill]}, got {routed_name}这个测试集不是一次写完就完了而是持续沉淀线上发现一次选错就把那条 query 捞出来加进测试集修复描述跑回归。我把它叫“技能回归库”一个技能如果没有 50 条以上的回归用例说明还没有攒到足够信心。5.2 观测链路一定要能看到“为什么这么选”真机上模型选错技能时你很难只通过最终回复判断问题出在哪。所以我给路由器加了一层日志记录用户原始 query、各技能得分、最终选中的技能和对应得分、注入的技能描述片段。有了这层记录迭代效率翻倍——不然就是在瞎猜。[ROUTE] query包邮吗 - refund_policy: 0.87, shipping: 0.82, order_query: 0.41 SELECTED: refund_policy通过这个日志我可以直观看到“哦原来是 shipping 的描述里没写包邮字样导致分数低”。补上之后问题消失。5.3 真机调优的节奏感我个人的推荐节奏是先修描述再修参数最后才动代码逻辑。前面几个月最容易犯的毛病就是遇到问题第一反应改代码改来改去发现模型根本不执行到这个代码块白白浪费半天。还有一种情况是技能本身有问题但模型就是一直调用说明描述里写了太多“诱人”的能力字眼。把描述中的范围收紧让技能只在特定意图下被选中能省掉不少幻觉式的调用。6. 技能到底是什么形态的内容价Agent 工程人员最该转变的一个思路最后我说点更抽象的东西。做 Agent 一年多我最大的思维转变是不要试图让自己扮演全知全能的上帝不要试图让 Agent 一次性解决所有问题而是要学会编排汇聚出来的技能。技能本身就是一种可复用资产它的生命周期甚至可以独立于某个业务线。企业里的“查库存”“算毛利”“排产”“发通知”抽象成 Skills 之后新的 Agent 项目不再从零开发而是从已有技能库中挑选、组合、加一层编排。这才是 agent-skills 真正的价值——它不是某个框架里的一个小功能而是一种把组织能力模块化、资产化的思路。从我目前实践的经验看只要坚持“技能独立、描述精准、路由聚焦、结果可观测”这四个原则一个 Agent 项目的稳定性和维护效率都会有质的提升。如果你正被 prompt 越写越长、模型总是不按预期调用功能这类问题折磨不妨试试这整套 Skills 化的改造路径。
返回列表