ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent技能化:从Prompt堆叠到可交付的工程单元

AI Agent技能化:从Prompt堆叠到可交付的工程单元

1. 从"AI什么都能答"到"agent真能干活":技能化才是分水岭

过去半年我一直在折腾 AI agent,项目代号就叫 agent-skills,一个很朴素的愿望:让 agent 别再当"只会聊天的顾问",而是真正把事办成。最早我做了一个自动整理周报的 agent,Prompt 写得非常"满分作文",让模型读邮件、抽要点、排优先级、写总结,看起来天衣无缝。结果一跑真实数据就露馅:带附件的邮件它看不见,格式不统一的表格它经常猜错字段,偶尔还把上周的数据混进这周,整个周报的逻辑直接崩掉。

问题不在模型不够聪明,而在于我把"让它干活"想得太简单了。一个能稳定干活的 agent,靠的不是一段精心设计的 Prompt,而是一组明确的技能——什么时候调哪个工具、输入输出长什么样、边界条件是什么、失败了怎么兜底。这些凑在一起,才是 agent 的能力底座。今天这篇就把我拆 agent-skills 这套体系的过程完整写一遍,从技能定义、代码落地、调试评估到踩坑复盘。适合两类人看:一类是刚开始搭 agent 但发现"能跑通 demo、扛不住真实数据"的人;另一类是已经在做 agent 应用,想把自己的 Prompt 工程升级成真正的能力工程的人。

先说一个反直觉的结论:技能化不是把任务变复杂,而是把任务变简单。你给 agent 十段长篇 Prompt,它会表现得像个"记忆力不太好的实习生",每句话都听着对,但做着做着就忘了最初的约束。你把它拆成五个有明确输入输出的小技能,每个技能只做一件事、边界清晰、可单独测试,整个系统反而可靠得多。这就像带新人:一次交代十件事,他大概率搞砸;一次给一件带验收标准的事,他不仅能做完,还能在做的过程里形成自己的经验。

2. 我把"技能"定义成一个可交付的工程单元

很多人一提 agent 技能,第一反应是"给模型写一段擅长某方向的 Prompt"。这个理解不能说错,但太粗了,写出来的东西基本不可维护、不可测试、不可复用。我在 agent-skills 里把技能当成一个完整的工程单元来定义,它至少包含四层。

第一层是能力描述。这段描述不是给模型看的"角色设定",而是给 agent 编排层看的"服务目录"。它要回答三个问题:这个技能解决什么问题?在什么条件下可以调用?调用了之后能拿到什么结果?我见过太多人把能力描述写成"你是周报助手,擅长总结邮件中的要点",这对编排层毫无信息量。正确写法是:"技能:weekly-report-summarizer。适用场景:当用户请求生成本周工作周报,且已提供或可访问邮件数据时。输出:结构化 Markdown,包含工作项、优先级、风险提示三个必填区块。"这样编排层才能做技能匹配。

第二层是工具协议。技能不是让模型空口说,而是让模型操作真实工具,所以必须定义清楚工具接口。这一步最容易踩的坑是给了模型工具却不说清参数约束。举个例子,我有个技能要查内部工单系统,一开始接口定义就写了 get_ticket(ticket_id),结果模型经常把 "TIK-1024" 传进去,而系统要求的是纯数字 1024。后来我在接口定义里加了两条:参数必须是整数,如果传入的值以非数字前缀开头,先剥离前缀再调用。差这一句话,成功率从六成提到了九成以上。

第三层是约束条件。包括使用该技能的触发前提、被禁止的操作、必要的前置检查。没有这一层,agent 很容易在边界场景里做出"看起来合理但实际荒谬"的操作。比如某个技能能从在线知识库拉取文档,如果不约束"仅当文档权限校验通过后才能读取正文",模型可能绕过权限直接拼接 URL 拿内容。这不仅是工程问题,还涉及数据安全。我在每个技能里都放了一个 precondition 检查的钩子,由固定代码执行,不靠模型自觉。

第四层是验证与兜底。技能执行完,怎么判断结果可不可信?至少要有结构校验和异常兜底两个机制。结构校验是硬性的,比如周报技能必须输出三个区块,缺了就算失败;异常兜底是策略性的,比如外部接口超时,技能内部要决定是重试、降级返回缓存还是明确报错。

一个技能要能长期服役,这四层缺一不可。少任何一层,短期看 demo 没事,长期跑真实场景必崩。我把这个完整定义落实成了一段 JSON Schema,每个技能在注册进技能库之前,必须通过这个 Schema 的校验。校验不过就返工,不允许带病上线。

2.1 为什么不靠 Prompt 硬撑

有人问,为什么不能让模型自己读完 Prompt 就"悟"出技能?问题在于不可控。模型的输出天然有概率性,同样的输入这次对、下次错;你把任务拆成技能并给每个技能配好工具、约束、验证,等于把概率性问题改造成确定性流程。对于稳定性要求高的生产场景,宁可让流程复杂一点,也要让结果可预期。

另外,技能化之后,每个模块都能单独升级。模型厂商更新了底层模型,你不需要重写整个应用,只需要重新跑一遍技能测试集,看哪些技能退化、哪些技能变强,然后针对性调整。这是 Prompt 堆叠给不了的维护性。

3. 手写一个最小技能:从需求分析到注册落库

空谈定义没有用,我拿一个真实场景走一遍完整的技能开发流程。当时我做一个内部信息收集 agent,需要从多份 PDF 合同里抽取关键条款生成摘要。这个需求听起来很简单,就是"读 PDF,抽条款,输出摘要",但落地时有一堆细节。

先说需求分析。我列了几条硬性要求:输入是 PDF 文件路径列表;输出是条款摘要表,字段包括合同编号、签约方、金额、有效期、特殊约定;处理失败的单份文件不能拖垮整个批次,要在结果里单独标注。这里有个关键取舍——摘要以抽取为主,不要求模型做深度归纳。因为合同是高频业务文件,我宁可结果结构化、字段可靠,也不要模型自由发挥写出漂亮的散文。这个决策后来被证明极其重要,自由发挥的抽取结果在真实合同上经常张冠李戴。

然后是工具层。我调研了市面上几个 PDF 解析方案,最终选了先转文本再做抽取的路线。原因很简单:合同这类文件大多是文字版 PDF,转文本后质量还行,流程也容易调试。如果是扫描件,就得走 OCR,那是另一套技能了。我在技能里封装了两个函数:extract_pdf_text(file_path) 和 extract_contract_fields(text),前者处理文件解析,后者负责干净地隔离"调用模型"这唯一一个不稳定的环节。这样如果结果出错,我能很快定位是解析问题还是模型问题。

这里有个我在 agent-skills 里反复强调的原则:把不确定因素隔离在最小范围内。一整段 agent 流程里,唯一可以接受"概率性输出"的地方就是模型推理这一步,其他环节——文件读取、格式转换、字段校验、结果落库——全部应该是确定性的。否则哪天线上出问题,你连排查的入口都找不到。

3.1 模型调用的正确姿势

抽取技能里最核心的一段逻辑,是构造模型调用的输入。我试过两种方式,效果差异巨大。

第一种是把整个合同文本一股脑塞给模型,让它"读完后抽取字段"。失败在于长文本会让模型注意力涣散,五千字的合同里,关键条款经常分布在不同的章节,模型容易漏掉藏在附录里的金额约定。第二种是先做规则切块,把文本按"合同主体信息""付款条款""期限条款"等关键词分段,再让模型针对每个分块抽取。效果立刻上了一个台阶。

为什么会这样?模型在处理超长输入时,注意力分布不是均匀的,前面的内容和靠近结尾的提示词会抢走大部分权重,中间的关键信息容易被"淹没"。分块之后,每一段输入短而聚焦,模型相当于从"通读全文找重点"变成"带着问题看指定段落",准确率自然高。

# 分块抽取的核心思路(简化版) def extract_contract_fields(full_text): sections = split_contract_by_keywords(full_text) fields = {} for section_name, section_text in sections.items(): prompt = build_extract_prompt(section_name, section_text, FIELD_RULES) result = call_llm(prompt, temperature=0) fields[section_name] = parse_structured_result(result) return validate_required_fields(fields)

注意上面这版实现里有个小细节,每个分块的 prompt 是从统一的 FIELD_RULES 生成的,而不是各自写死。这样如果业务上要调整字段定义,只改一份规则,所有分块自动生效。我一开始没做统一规则,结果三个分块各管各的,金额格式都不一致,上线第二天就被业务方打电话问"为什么同一个合同两种金额格式"。后来把所有提示约束收敛成一份规则文档,才真正解决。

3.2 技能注册:让编排层认识新成员

技能写好之后,要注册进 agent-skills 的仓库。我的注册信息就是第二节说的四层定义,存成 JSON,放在 skills/catalog/ 目录下。注册时有一道自动检查,验证技能描述里有没有写清触发条件、工具接口参数有没有带类型、验证规则能不能在测试集上通过。这套检查里最苛刻的一条是:技能必须自带至少三个测试用例才能进入运行环境。没有测试用例的技能,哪怕写得再漂亮,也不允许上线。这条规矩是我被坑了好几次之后才定的,后面专门有一节讲测试,这里先按不表。

注册完成之后,编排层就能在运行时通过技能描述匹配到它。匹配逻辑我用的不是让模型自己选,而是两阶段:先基于关键词和向量召回候选技能,再用模型做一次轻量的意图确认。两阶段的好处是,召回的快和便宜,确认的准和贵,两者结合,既不至于每次都把所有技能描述塞给模型(费 token 还容易乱),也不会完全靠关键词匹配导致语境错配。

4. 技能库的组织方式:粒度、命名与依赖

单技能上线之后,下一个问题是技能多了怎么办。我大概在做到第八个技能时开始感受到明显的管理混乱:有的技能重叠严重,有的技能边界模糊,有的技能想调用别的技能却找不到入口。agent-skills 到这一步才真正需要设计一套"组件化"的体系。

先说粒度。我的经验是,一个技能最好只对应一个可独立验证的业务动作。比如"解析合同 PDF"和"从合同文本抽取字段"是两个技能,而不是一个。前者可以被 OCR 技能复用,后者可以被应付账款的流程复用。技能越靠近原子能力,复用价值越高。但原子化也不能走极端,否则编排层会陷入"技能太多、编排太碎"困境。我现在的标准是:如果某个技能在业务上永远只被一个流程使用,且它拆分前后的测试成本差不多,那就不拆。

命名规范我踩过不少坑。一开始我起名很随意,什么 do_extract、contract_parser、parse_contracts 都有,结果一个月后我自己都分不清 contract_parser 和 parse_contracts 有什么区别。后来统一成 <领域>-<动作>-<对象> 的格式,比如 contract-extract-fields、contract-parse-file、email-list-unread。命名最大的价值不是好看,而是让"技能描述"里可以明确引用其他技能的名字,编排层才能准确建立依赖关系。

依赖管理是技能库设计中容易被忽略的一环。我的第四号技能(合同摘要)依赖第一号技能(合同解析),在注册信息里就要显式声明。这样当一号技能的接口发生变更时,检查工具能自动扫描出哪些技能受影响,提醒我跑一遍回归测试。上次我调整了 PDF 解析的返回结构,要是没有依赖声明,契约抽取技能会在静静里坏掉,直到业务方反馈才发现问题。有了自动检查,五分钟就能定位完所有受影响的技能。

4.1 版本管理:让技能可回滚

技能既然是一个工程单元,版本管理就不能少。我给每个技能目录做了 semver 风格的版本号,主版本变化代表接口不兼容,次版本代表行为有变化但接口不变,补丁代表纯内部修复。变更记录写在一个 CHANGELOG 里,一句话说明改了什么、为什么改。

这个习惯看起来笨重,但在一次事故里救了我。当时我在优化邮件摘要技能的 Prompt,模型从旧版本换成新版本后,摘要风格大变,从简洁条目式变成了长篇叙述式,业务方直接炸了。因为我对技能做了版本管理,一分钟内就回滚到了上一个补丁版本,线上恢复。事后我用 diff 工具对比了两个版本的输出差异,才发现是新模型对"风格"这个词的敏感度完全变了。从那以后我养成了一个习惯:每次升级底层模型,都必须跑全量技能的回归测试,而不是只看一两个样例就放行。

5. 技能可靠性的核心:测试集、回归与可观测性

前面几次提到测试集,这是 agent-skills 这套体系里我认为最值得投入的部分。很多 agent 项目失败,不是模型能力不够,而是没有"可重复的验收标准"。写代码有单元测试,为什么 agent 技能没有?我在早期总觉得 agent 这种东西测不准,输出是自然语言,怎么断言对不对。后来想通了,我们不是要断言模型的每一句话,而是断言技能行为的关键约束。

给技能写测试,重点测三类行为。第一类叫主路径正确性:给标准输入,期望输出中必须包含哪些关键字段、格式是否符合规范。第二类叫边界鲁棒性:空输入、超长输入、格式异常的输入,技能不能直接崩溃,要按预案返回错误。第三类叫约束恒定性:比如"金额字段不得出现负值""权限不足时必须拒绝访问"这类永远不变的铁律。这三类行为都设计成可自动断言的,我的测试代码里大量用了字段存在性检查、正则匹配、数值范围校验,而不是让另一个模型去"打分"。

为什么要强调可自动断言?因为只有自动化,回归测试才能在每次改技能之后可靠跑起来。我自己的流程是:在技能代码增加一个 pytest 风格的测试文件,每条测试用明确的 assert 语句。提交代码时先跑一遍测试,通过后再合并。到后期,整个技能库有上百条测试,跑完不到一分钟,但每次发布前我一定会跑,哪怕只改了一行 Prompt。理由很简单:Prompt 这种"看起来不会坏"的改动,恰恰最容易让行为偏离。

5.1 可观测性:看不到过程,就谈不上调试

测试解决"结果对不对"的问题,可观测性解决"过程发生了什么"的问题。agent 技能跑在真实数据上,输出错了,你总得知道错在哪个环节。我在每个技能里都埋了结构化日志:模型调用的输入摘要、token 消耗、输出结果、耗时、每一步的中间产物。日志统一写到一个问题追踪用的数据表,按请求 ID 关联。

有一次用户反馈某个合同的生效日期被抽成了"1970年1月1日",明显是错了。如果没有日志,我根本不知道要不要怀疑模型;但日志显示,接收到的合同原文里该字段本来就是空值,抽取代码在没有值的情况下给了个默认时间戳,导致错误输出。问题根本不在模型,而在我初始化的默认值。修掉这个默认值,同类问题全消失。没有日志,这类问题可能要排查一下午。

可观测性的另一个价值是发现"暗病"。统计一段时间内各技能的失败率、重试率、平均 token 消耗,你会发现一些很有意思的规律:某个技能的失败率总在下午三四点上升,因为下游系统在那个时段做备份,响应变慢;另一个技能在月初 token 消耗暴涨,因为业务量天然有波峰。知道这些,你才能决定要不要加超时重试,要不要做限流,要不要在月初提前扩容。别等出事了才看日志。

6. 半年实战踩坑复盘:技能粒度、语境泄露与工具幻觉

最后写几个真实的坑。每个坑我都付出过线上事故的代价,写出来希望能帮你绕过去。

坑一:技能粒度太粗,导致技能内部逻辑不可复用。最初我把"生成项目周报"做成了一个技能,里面包含了拉数据、汇总、格式化、发送邮件四个动作。听起来没问题,但后来我发现另一个流程"生成个人日报"也需要"拉数据+格式化",却没法复用周报技能,只能复制一份逻辑。等到周报技能里加了权限校验,日报那边忘了同步,造成了数据可见范围不一致的隐患。教训就是:在流程入口和原子能力之间,一定要预留一层薄薄的编排层,让技能保持原子性。

坑二:能力描述泄露了上下文,导致模型张冠李戴。这是最隐蔽的一个问题。我曾在技能描述里写"当用户是财务部成员时,返回含税金额",本意是描述适用条件。但真正跑起来,模型在技能匹配阶段看到整段描述后,会"理解"出一些不该有的语境,比如主动假设所有请求都来自财务部,或者把权限判断提前到匹配阶段做掉了,反而绕过了后面的安全校验。后来我把"能力描述"和"身份/权限信息"彻底分开,能力描述只谈功能和触发条件,涉及权限的全走确定性代码。这个改动让系统安全性提升了一个档次。

坑三:模型对工具参数的"幻觉式填充"。模型在调用工具时,偶尔会编造出不存在的参数。有次我的查询技能要求传日期范围,模型在没有上下文的情况下填了一个"上周一到现在",看起来合理,但其实是凭空生成的,直接导致查询范围错误。这个问题的解法有两层:一层在工具协议里写清楚参数的允许取值枚举,能用选项就不用自由文本;另一层在做工具调用前的参数校验,凡是不符合 Schema 的直接打回重生成,而不是硬着头皮调工具。这两层叠加之后,参数层面的幻觉基本清零。

坑四:用"新模型一定更好"替代回归测试。有一次某新版本模型发布,我直接切换过去,手测了几个 demo 样例感觉不错,就上线了。结果第二天发现摘要技能的输出风格全变了,业务方措手不及。问题不在于新模型差,而在于我没有验证"新模型在你的技能体系里是否保持行为一致"。后来我把模型升级跑回归测试固化成标准流程,坚决不等出问题才想起来测。模型能力是变量,技能体系是系统,系统不能因为变量波动就失去稳定性。

坑五:只测 happy path,忽略异常路径。早期的测试用例全是我精心构造的"完美输入",自然全过。真实数据从来不会配合你:合同缺页、日期格式是"二零二三年"、金额写成"1,000元"带中文逗号。后来我专门收集了真实业务中的异常样本,做成坏数据测试集,每一个都逼着技能要么正确处理、要么明确报错。现在我的测试集里,不正常数据比正常数据还多,这反而让我对技能上线的信心更足。

这些坑之间其实有一条共同的底层逻辑:别把可靠性建立在模型对自然语言的理解上,要建立在确定性的工程约束上。技能化、测试化、可观测化,每一项都是在把模型的概率性行为"关进笼子"里,让它在可控的范围内发挥能力。模型负责聪明,工程负责可靠,两者结合,agent 才真正从"能聊"变成"能干"。

我在实际维护 agent-skills 这套体系时,最大的体会是:它本质上不是一个代码仓库,而是一套关于"如何约束智能系统"的方法论。技能的数量从三五个涨到三五十个,核心原则始终没变——每个技能清楚自己要什么、能做什么、做不成怎么收场。接下来我大概会继续往两个方向扩展:一个是让技能具备自我学习的闭环,把线上失败样本自动回流成新测试用例;另一个是把技能描述做成可以被 agent 之间互相发现和调用的开放协议。这两个方向都还在探索,等有了阶段性成果,再回来继续分享。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进