☰
Agent技能化架构设计:告别Prompt失控,打造可编排的AI助手
2026/9/25 8:42:58 网站建设 项目流程

之前说过一句话,一直憋到现在:很多Agent项目不是死在模型能力上,而是死在"什么都能聊,但什么都做不精"。用户打开你的Agent,不是来听它讲道理的,是来让它干活的。可你把一堆能力全塞进Prompt里,模型就开始表演失忆,指令一多反而什么都抓不住。一年前我折腾的这个项目,代号就叫agent-skills,核心思路很简单:把Agent的能力从"Prompt里的描述"拆成"一个个可注册、可调用、可编排的技能"。这篇把整套设计思路、踩坑过程和代码实现都整理出来,给正在搭Agent的同行做个参考。

1. 为什么我放弃了"全知全能"Prompt,转而给Agent搭技能库

先说几个真实场景,你大概率也遇到过。

第一个是客服类Agent。最开始把所有业务规则写进system prompt,大概两千字,效果还不错。后来产品要求加退款流程、物流查询、优惠券计算,Prompt涨到五千字,模型开始答非所问,用户问A问题,它能扯到B业务上。我把规则精简、重排、加分隔符,折腾了整整两周,效果依然不稳定。

第二个是内部效率工具类的Agent。我给它接了日历查询、待办管理、邮件草稿、周报生成,能力都在一个Prompt里描述。结果它经常分不清该调哪个能力,明明有日历查询技能,用户问"明天几点开会",它非要现编一个答案。更要命的是,各个能力的输入输出格式不统一,返回结果堆在上下文里,模型自己都不知道哪段数据是哪个请求的。

第三个更典型,是把Agent从"演示环境"推到"生产环境"时的问题。你自己测试的时候,一个能力跑通了,感觉一切OK。但真实用户会乱问、会打错别字、会提边界条件,模型处理不了的时候不会说"我做不到",而是硬编一个让人哭笑不得的结果出来。

三个问题指向同一个根因:你根本没给Agent定义清楚"能做什么"和"怎么做",而是把所有可能性都塞给了一个概率模型去猜。

1.1 单体Prompt的失控过程

你可以把单体Prompt想象成一个新员工的入职培训手册,里面写了"你要为用户提供全方位的帮助",然后附了五百条业务规则。这个新员工脑子确实好使,但每处理一个问题,它都要把五百条规则从头到尾过一遍。规则越来越多,它就开始混淆,把退款规则套到物流问题上,把优惠券计算逻辑套到售后场景里。

而且单体Prompt还有个隐藏问题:所有能力共享一段上下文。日历查询的返回结果、邮件草稿的生成内容、周报的模板,这些信息全部混在一起。模型在处理当前任务时,注意力会被无关信息干扰。这就好比一个程序员同时开着五十个项目的上下文,切换任务时很难不串线。

另一个失控点是调试困难。Prompt出问题,你根本定位不了是哪条指令导致的。改一句话,可能影响所有场景的表现。加一个新能力,可能把旧能力的触发条件挤掉。这种"牵一发而动全身"的改法,在业务快速迭代时基本是灾难。

1.2 技能化架构的核心逻辑

agent-skills的思路,是把每个能力做成一个独立的"技能",每个技能有自己清晰的输入、输出、触发条件和边界约束。Agent不再"猜测"自己该干什么,而是从一个登记在册的技能列表里做选择和组合。

我可以用一个很土但贴切的比喻:单体Prompt像让一个万能助理同时管行政、财务、技术、客服,他忙不过来;技能化架构则是给助理配了一堆专门的工具和应用,每个工具只解决一类问题,助理要做的事情是——知道什么场景用哪个工具,并且按顺序把工具串起来。

这套架构带来三个直接好处:

  • 边界清晰。每个技能只对自己的输入输出负责,上下文不再大锅烩。日历技能返回的数据不会污染邮件技能的判断。
  • 独立迭代。技能之间互不干扰。修搜索技能的bug,不会影响周报生成技能的稳定性。
  • 可观测可审计。Agent调用过哪些技能、传了什么参数、返回了什么结果,全部有日志。出问题能回溯到具体环节。

后面我会逐层拆解,这套技能库的完整设计,以及每一步的代码实现逻辑。

2. 技能到底长什么样:拆开看一个Agent技能的五要素

我在agent-skills里对"技能"的定义,经历了三个版本的迭代。第一版很简单,就是一个函数加一段文字描述,跑通后发现模型经常选错技能。第二版加了参数JSON Schema,选对率明显提升,但遇到边界情况还是会翻车。第三版才沉淀出稳定的五要素结构,这版在多个项目里都稳定运行。

一个完整的技能,包含五个部分:

  • 能力声明(name + description):技能叫什么、在什么场景下使用、有什么限制。
  • 触发条件(triggers):什么样的用户意图应该路由到这个技能。
  • 参数规范(parameters Schema):调用这个技能需要传什么参数,每个参数的类型和约束。
  • 执行逻辑(execute):技能被调用后实际执行的函数或接口。
  • 边界约束(guardrails):超时时间、重试策略、失败后的兜底行为、权限范围。

这五要素里,最容易被人忽略的是边界约束。很多人定义一个技能就写个name和description,然后直接接函数,结果线上跑着跑着,某个技能因为外部接口超时,直接让整个Agent挂掉。我后面会用一整章讲这块踩过的坑。

2.1 五要素各自的作用

  • 能力声明是给Agent看的"广告词"。你描述得越准确,模型选技能的选择就越准确。这里有个容易被忽略的技巧:描述里一定要写清楚"不适用什么场景",负面示例能大幅提升选择准确率。只写正面场景,模型容易把相关但不完全匹配的问题也路由过来。
  • 触发条件是给路由模块看的"分流规则"。它可以是关键词规则、语义相似度阈值,也可以交给模型做意图判断。触发条件写得越具体,路由越稳。
  • 参数规范是给参数校验器看的"合同"。模型生成的参数必须先通过Schema校验,才真正传给执行函数。这样能拦截掉大量非法输入。
  • 执行逻辑是真正干活的代码。它不一定要是Python函数,也可以是HTTP接口、SQL查询、外部服务调用。关键是执行逻辑必须和参数规范严格对齐。
  • 边界约束是给系统的"保险丝"。超时就断、失败就降级,绝不能让技能执行失败变成Agent的幻觉素材。

2.2 一个完整的技能定义示例

以"查询天气"技能为例,第五版的设计大概是这样的:

skill_weather = { "name": "query_weather", "description": "查询指定城市当前天气及未来3天预报。当用户询问天气、气温、降雨、风力等情况时使用。不适用于查询历史天气、空气质量指数或台风路径,这些请使用其他技能。", "trigger_keywords": ["天气", "气温", "会不会下雨", "风力", "天气预报"], "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "城市名或行政区名,如北京、上海浦东" }, "days": { "type": "integer", "minimum": 1, "maximum": 3, "default": 1 } }, "required": ["location"] }, "execute": fetch_weather_from_service, "timeout": 5, "retry": 1, "fallback": "天气服务暂时不可用,请稍后再试" }

这段定义里,最值得玩味的是description里的负面声明。写了"不适用于查询历史天气、空气质量指数或台风路径",模型的技能选择准确率能提升不少。原因后面会细讲。

2.3 技能分类:感知、操作、推理与交互

技能不是只有"调用API"一种形态。我跑了几个项目后发现,把技能按职责分类,对后续编排和复用非常关键。我最后常用的分类是四类:

技能类型典型职责示例交互特点
感知类获取外部信息天气查询、新闻检索、数据库查询只读,不改变系统状态
操作类改变系统状态创建工单、发送邮件、更新订单有副作用,需要授权确认
推理类纯计算或逻辑判断价格计算、日程冲突检测、代码执行依赖明确参数,无外部副作用
交互类向用户询问补充信息收集订单号、确认操作意图多轮对话,需要挂起恢复

这四类技能在编排时的待遇完全不同。操作类技能我会强制增加用户确认步骤,交互类技能会设计专门的上下文挂起机制,推理类技能则优先用确定的计算代码而不是让模型自己算。这些细节直接决定了Agent在生产环境下的可靠度。

3. 从定义到上线的完整链路:注册、编排、调用与降级

定义好单个技能只是第一步。真正让Agent具备"干活能力"的,是一整套从注册到调用的运行链路。这一章我把agent-skills运行时核心的几个环节展开讲,全部是可直接复用的工程实践。

3.1 技能注册表与模型决策

每个技能定义好之后,第一步是注册到一个全局的技能注册表里。注册表本质就是一个字典,key是技能名,value是整个技能定义对象:

class SkillRegistry: def __init__(self): self._skills = {} self._name_to_keywords = {} def register(self, skill: dict): name = skill["name"] self._skills[name] = skill for kw in skill.get("trigger_keywords", []): self._name_to_keywords.setdefault(kw, []).append(name) def match_by_intent(self, user_query: str): # 优先关键词精确匹配 for kw, names in self._name_to_keywords.items(): if kw in user_query: return names return None registry = SkillRegistry() registry.register(skill_weather) registry.register(skill_create_ticket) registry.register(skill_calculate_price)

这里有个设计取舍值得说:技能选择到底用确定性规则,还是让模型来选?我的实践结论是分层过滤。先用关键词和规则做粗筛,得到候选技能列表;如果粗筛结果唯一,直接执行,不浪费模型调用;如果粗筛结果有多个或为空,再让模型从候选列表里做最终选择。这种方式既省Token,又比纯规则或纯模型判断都稳定。

3.2 参数校验与失败兜底

模型选择了技能后,紧接着就是参数校验。这一步必须用JSON Schema严格校验,不能依赖模型"自觉"传参数。我在实际项目中遇到过模型把日期传成"明天"、把金额传成带逗号的字符串、把必填字段漏掉等情况,全是在校验层拦下来的。

校验通过后,执行器会做几件事:

  • 记录调用日志,包含入参、时间戳、调用方会话ID;
  • 按技能配置的超时时间执行函数,默认5秒;
  • 如果超时或抛异常,按retry次数重试,默认1次;
  • 最终失败时,返回一个标准错误信息,而不是让异常直接上抛。

错误信息格式我固定为:

{ "status": "error", "error_code": "weather_service_timeout", "message": "天气服务暂时不可用,请稍后再试" }

之所以用这种结构化错误格式,是为了让Agent在拿到错误结果时能明确知道"技能失败了",而不是把错误信息误当成正常数据继续编故事。这一点我会在踩坑章节重点解释。

3.3 一次真实调用背后的完成流程

把完整链路串起来,一次用户请求到最终回应的流程是这样的:

  1. 用户输入"上海明天会不会下雨?"
  2. 路由模块用trigger_keywords粗筛,命中query_weather技能。
  3. 参数提取器从用户输入里抽取出 {"location": "上海", "days": 2}。这里的抽取可以交给模型,但必须有Schema约束。
  4. 参数校验器检查通过。
  5. 执行器调用天气服务,拿到返回数据。
  6. 结果规整器把外部数据转成Agent友好的格式: "根据天气服务,上海明天(2025-01-15)有小雨,气温5~9℃,东南风3级。"
  7. 结果回填到Agent的上下文窗口,Agent基于这段真实数据生成用户可读的回复。

这个流程里,第6步经常被忽略,但特别重要。外部服务返回的原始JSON通常包含大量无关字段,直接塞进上下文既浪费Token又会干扰模型。我在agent-skills里为每个技能配了一个result_formatter函数,负责把原始返回精简成一段话。这一步做好,模型回复的准确率能肉眼可见地提升。

4. 技能编排实践:让多个技能像团队一样协作

单技能场景其实不太需要Agent参与,规则引擎就够了。Agent真正体现价值的地方,是多个技能组合起来完成一个复杂目标。这一章讲编排的几种模式,以及我在实战中的观察。

4.1 四种基础编排模式

在agent-skills里,我总结了四种基础编排模式:

  • 顺序编排:一步做完做下一步,上一步的结果是下一步的输入。典型场景是"查日历确认空闲时间,再创建会议邀请"。
  • 条件分支:根据某个技能的结果,决定走哪条分支。典型场景是"查询库存,如果低于阈值就去创建补货工单,否则提示用户一切正常"。
  • 并行编排:多个技能互不依赖,同时执行,节省时间。典型场景是"查天气"和"查航班"同时进行。
  • 循环编排:对一组数据逐个执行同一个技能。典型场景是"批量检查多个服务的健康状态"。

这些模式在代码层面就是普通的控制流,但关键问题是:谁来编排?我试过两种策略,效果差异巨大。

4.2 周报自动生成场景的编排拆解

拿我做过的周报Agent当例子。这个Agent的核心目标:用户说"帮我生成这周的周报",它要自动汇总本周的日程、代码提交记录、待办完成情况,然后生成一份结构化的周报。

我最初设计为链式调用:

def generate_weekly_report(user_id): # Step 1: 查询本周日程 events = query_calendar(user_id, "week") # Step 2: 查询代码提交记录 commits = query_code_commits(user_id, "week") # Step 3: 查询待办完成率 todos = query_todo_stats(user_id, "week") # Step 4: 汇总生成周报 report = generate_report(events, commits, todos) return report

第一个版本就是把四个技能用Python代码固定编排好,顺序执行。效果很稳,但有个问题:用户的需求一旦变化,比如"只汇总日程和待办,不要代码记录",代码逻辑就要改。

后来我改成了"模型生成计划+代码执行计划"的方式。模型先看用户意图,生成一个技能执行计划,比如:

[ {"skill": "query_calendar", "params": {"user_id": "...", "range": "week"}}, {"skill": "query_todo_stats", "params": {"user_id": "...", "range": "week"}}, {"skill": "generate_report", "params": {}} ]

然后一个计划执行器按顺序执行这些技能,再把结果汇总。这种方式灵活度高,但稳定性比固定代码差一截,需要在编排层加约束。

4.3 动态编排:让模型当指挥的得失

模型动态编排最大的坑在于它会自作主张。我遇到过模型在生成周报计划时,自己发明了一个不存在的技能,叫"get_weekly_summary",注册表里根本没有。还有一次,模型擅自改变了技能执行顺序,先调生成报告再查数据,结果报告里全是空的。

后来我加了两道保险:

  • 只允许从注册表已有技能中选择。模型生成的计划,必须在已注册技能集合内,否则直接拒绝并让模型重新生成。
  • 计划校验器。用Schema定义"计划"的格式,校验技能名是否存在、参数是否合法、是否包含必选步骤。

加了这两道保险之后,动态编排的稳定性基本赶上了固定代码编排,同时保留了灵活度。我现在的建议是:核心流程用固定代码,非核心或长尾需求用动态编排,两者结合的收益最高。

5. 实测踩坑记录:技能冲突、上下文污染与幻觉兜底

技能化架构不是银弹,它只是把一部分不确定性从模型侧转移到了工程侧。工程侧的坑一点也不少。这一章是全文最硬核的部分,我把实测踩过、又逐一解决的坑位写清楚。

5.1 技能描述写太"泛",模型总选错

这是第一个版本遇到的问题。我一开始把query_weather的描述写成"查询天气信息"。结果用户问"上海适合穿什么衣服",模型居然把它路由到了query_weather。表面看也没错,但查询天气返回的只有气温和降水,根本没有穿衣建议,模型只能硬编一个。

后来我研究了一下模型选择技能的行为模式,发现它特别吃"边界描述"。所以我给每个技能的description加了正反两面描述:

正面:当用户询问天气、气温、降雨、风力等情况时使用。 反面:不适用于穿衣建议、空气质量、台风路径、历史气候统计,这些请使用或路由到其他技能。

这个改动让技能选择准确率提升非常明显。为什么?因为负面描述减少了语义空间的不确定性,模型在匹配时有了"排除项",不再把相似但不等同的意图拉进来。

5.2 上下文污染:模型分不清"哪个结果对应哪个请求"

并行编排或连续编排多个技能时,所有技能的结果都堆在上下文里,模型经常分不清哪段数据是哪个技能的。一开始我以为这是小事,后来发现后果很严重。

最典型的一次事故:用户先问"北京天气怎么样",再问"那北京明天呢"?Agent先调query_weather("北京", 1),又调query_weather("北京", 2),两次结果都在上下文里。模型在生成回复时,引用了第一天的数据回答第二天的天气,而且语气非常确定。

解决方案是在结果规整阶段,给每段技能结果加明确的标签前缀:

[技能: query_weather | 参数: {"location": "北京", "days": 2} | 时间: 2025-01-14 10:30:22] 天气服务返回:北京明天(2025-01-15)有小雨,气温5~9℃。

这样一来,模型能清楚区分"哪个请求的哪个返回"。加标签之后,上下文污染导致的错答问题基本消失了。

5.3 幻觉兜底:技能失败后模型编答案

这是所有坑里最危险的一个。技能执行失败,返回了错误信息,但模型拿到错误信息后,不会老实说"查询失败",而是会基于错误的上下文"猜测"一个合理答案。比如天气服务超时,模型直接回答"北京明天多云,气温8℃"——它自己编的,语气还特别笃定。

我排查了很久才发现,问题出在错误信息的措辞上。我之前返回的错误JSON是:

{ "status": "error", "message": "服务超时" }

模型看到这段文字,可能理解成"这是用户的查询结果"。后面我改成强语义的错误隔离格式,并且在回填上下文时,对错误结果前缀加上明显的"阻断标记":

[执行失败] 技能 query_weather 调用异常:天气服务超时。请不要猜测或编造结果,直接告知用户服务暂时不可用。

同时,在系统提示里写死一条规则:"当技能返回状态为error时,严禁编造任何数据结果。"这一套组合拳下来,才把幻觉应答率压到可接受的范围。

5.4 常见坑位速查表

把这一章踩过的坑整理成一张表,方便直接排查:

现象根因解决方案
技能选择不准description缺少负面示例正反两面写描述,明确排除场景
模型传非法参数未做Schema校验加JSON Schema严格校验层
多个结果混淆上下文缺少结果标识结果回填前加技能名+参数标签
技能失败后编答案错误信息被当成正常结果强语义错误隔离+系统提示禁止编造
动态编排乱序模型自由度高计划Schema校验+只允许注册表内技能
一个技能报错整站不可用缺少超时与降级统一timeout+retry+fallback

这张表是我每次接入新项目时都会拿出来核对一遍的清单,省了很多线上下排查的时间。

6. 技能库的演进方向:复用、评测与生态化

agent-skills从最初的一个简陋脚本,变成一个勉强能称为"框架"的东西,中间最大的转折就是我意识到:技能的维护成本和技能的复用价值,决定了这套架构能不能活下去。技能写得再漂亮,如果不可评测、不可复用,那它只是又一套一次性代码。

6.1 独立评测是技能复用的前提

单个技能在集成到Agent之前,必须可独立评测。我在每个技能定义里强制加了一个evaluate函数,负责跑一组预先设计的测试用例。比如query_weather的评测集至少包含:

  • 正常查询:"北京明天天气如何" → 期望命中query_weather,且参数解析正确
  • 边界输入:"北京和上海哪个更冷" → 期望不直接命中单城市天气技能,可能需要对比逻辑
  • 负面场景:"推荐一件适合北京的羽绒服" → 期望不路由到query_weather

评测逻辑很简单:跑一组输入,看技能路由选择对不对、参数解析对不对、最终输出质量如何。只有通过评测的技能,才有资格注册到生产环境的技能库里。这套机制让我敢放心地加新技能,不用怕污染已有的能力。

6.2 观察指标:调用成功率、延迟与Token消耗

技能上线后,监控指标是另一个关键环节。我每个技能挂钩了三个核心指标:

  • 调用成功率:成功执行的次数 / 总调用次数。
  • 延迟:从发起调用到返回结果的耗时,P50和P99都要看。
  • Token消耗:技能描述、参数Schema、结果回填一共吃掉多少上下文Token。

Token消耗是最容易被忽视的。一套大技能库,每个技能的description都可能几百字,把这些描述全部塞给模型,每次对话都要消耗大量Token。我后来采用了一种"按需加载"的策略——先用粗筛选出候选技能,只把候选技能的描述和Schema注入上下文,而不是每次把全部技能定义都塞给模型。这招让每次调用的Token成本降了将近一半。

6.3 从个人技能库到团队技能市场

当技能数量超过二十个之后,我开始有意识地把它当成一个"市场"来运营。团队里每个人都可以提交技能,但必须先满足三件事:有完整的五要素定义、通过评测集、有监控指标的报告。程序上收窄,就会倒逼质量。

这里有一个真实的体会:少即是多。我曾经维护过一个六十多个技能的库,里面一半技能一个月都没被调用一次。这些僵尸技能不仅占用上下文,还会干扰模型的选择。后来做了一个季度清理,把所有调用率低于1%的技能下线,Agent的整体回答准确率反而涨了一截。

我现在对技能库的目标,不再追求数量,而是追求每个技能都能回答三个问题:它能解决什么问题、它不能解决什么问题、它失败了会留下什么可追踪的痕迹。想通这三点,技能的体系就算立住了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询