Agent技能化设计:从上下文混乱到稳定可用的工程实践 前阵子好几个做 AI 应用的朋友不约而同地和我聊起同一个困惑Agent 项目跑通了 demo但一上真实场景就乱成一团——模型一会儿忘了前面的工具结果一会儿又重复调用同一个接口改 prompt 改到怀疑人生。我看了他们的代码之后发现问题几乎都出在同一件事上整个 Agent 只有一套大而全的 system prompt 和一堆散落的工具函数没有任何“技能”的概念。后来我把思路切换到“技能化”之后情况一下子清晰了很多。所谓 agent-skills不是让模型自己摸索着调用十几个函数而是把一类任务封装成一个带明确说明、带输入输出约束、带执行策略的“技能单元”Agent 需要做什么就加载什么顺序、边界、异常处理全都提前设计好。这套思路帮我省下了大量调 prompt 的力气也让项目从“demo 能跑”进化到“真实用户真的能用”。这篇就把我在技能化改造过程中的完整设计、代码实现、踩坑记录一次讲清楚。适合正在做 Agent 应用、被上下文混乱和多轮调用问题折磨的开发者参考也适合想从零搭一个可维护 Agent 体系的人了解整体脑图。1. 先搞清楚Agent Skills 到底是什么1.1 从“把步骤写进脑子”到“给 Agent 装工具箱”大多数入门级的 Agent 是这样工作的你在 system prompt 里写清楚“你是一个助手你可以使用以下工具”然后模型在对话中自己决定调用哪些工具、按什么顺序调用。这种做法的本质是把工作流程的全部信息都塞进上下文里让模型自由发挥。问题在于模型的注意力是有限的工具越多、任务越长就越容易出错。我在一个数据清洗项目里试过给模型开放 8 个工具前几轮对话还算正常到第 20 轮左右就开始频繁地重复读取同一个文件甚至把上一个任务的处理逻辑套到当前任务上。后来我数了一下那套 system prompt 加上历史消息加起来快 6000 token 了模型其实已经“看不过来”。agent-skills 的思路正好反过来。它不把所有步骤都堆在提示词里而是把“做一个数据清洗报告”这件事拆成一个独立的技能模块。这个模块自带以下内容这个技能是干什么的、什么场景下用输入需要什么样的数据结构内部执行分哪几步每步调用什么工具失败时怎么重试、怎么回报错误。Agent 只需要在合适的时机加载这个技能技能内部自己安排具体动作。这就好比你不去训练一个新人记住公司所有流程而是给他一本手册遇到哪种任务翻到对应章节照做手册里的步骤还都是提前验证过的。1.2 技能、工具、提示词、工作流的边界不少人会把“技能”和“工具”混为一谈我在最初设计时也踩过这个认知坑。简单来说工具是技能的执行原子技能是“工具 编排逻辑 使用说明”的组合。举个例子工具search_web(query)、fetch_page(url)、extract_text(html)技能调研一个行业的最新动态——它规定先搜索、再打开排名靠前的 3 个页面、提取正文、做摘要如果搜索失败就换关键词重试一次。工具只回答“我能做什么”技能回答“这件事该怎么做”。提示词则是语言的载体它可以把一个技能的使用描述发给模型也可以在技能内部作为步骤指令存在。工作流和技能的区别更偏“严格程度”工作流像铁轨步骤顺序和分支完全预设几乎没有模型发挥空间技能像半自动辅助驾驶定义了大方向和边界中间几步允许模型做小决策但不会让它频繁脱轨。我在实践中的感受是纯工作流适合稳定且高频的内部流程技能更适合需要结合模型判断力、又希望可控的开放场景。1.3 什么时候值得引入技能系统不是所有项目一开始就需要技能系统。如果你的 Agent 只有两三个工具任务链条也很短直接在 prompt 里写清楚就足够了。但当你出现下面任一信号就是引入技能系统的时机工具数量超过 5 个模型开始混淆功能或调用错参数同一类任务需要在多个项目里复用你不想每次重新写 prompt任务链条超过 3 步比如“搜索 → 摘要 → 写报告 → 发邮件”你希望给 Agent 加上权限边界某些高危动作只允许在特定技能内部触发。我遇到过最典型的例子是文档处理类 Agent读文件、解析格式、提取字段、校验数据、生成表格这五个动作如果让模型裸调顺序经常乱把它们封装成一个“文档解析技能”准确率直接从 67% 提升到 91%。这个提升不来自模型变聪明了而是因为顺序被固定模型不需要每一步都猜下一步做什么。2. 技能系统的整体设计思路2.1 技能的本质输入-处理-输出的闭环我在设计 agent-skills 时反复琢磨一句话技能是模型可以按需调用的、有边界可预期的能力单元。它应该像一个黑盒外面只暴露明确的接口内部可以很复杂但不需要让模型感知。为了让模型能正确使用技能技能自身包含四个核心要素名称和描述让模型判断“现在是否需要它”输入参数定义模型需要提供什么信息内部执行逻辑可以是一段固定代码也可以是一个局部 prompt 工具调用链输出格式和退出条件执行完返回什么失败时怎么反馈。这四个要素缺一个都会出问题。我最初只写了名称和输入没有定义输出格式结果模型经常把技能的中间状态当成最终结果直接截断给用户看。后来我在每个技能里都加上了output_schema这个问题立刻消失了——模型知道什么时候算“完成”。2.2 技能的三层结构定义层、执行层、编排层整个技能系统我在逻辑上分成了三层这样既方便单独测试也方便后续扩展。第一层是定义层负责描述技能本身。包括技能的唯一 ID、一句话简介、参数列表、返回结构、适用的任务类型、停用条件。这一层是给模型“看”的所以描述要写得像一个优秀 API 文档避免含糊词汇。第二层是执行层负责真正干活。它可以是一个 Python 函数也可以是一段带子流程的指令序列。我的经验是只要是逻辑固定的操作如数据处理、API 调用、文件读写尽量直接用代码实现不要交给模型只有需要理解语义的部分如判断文本情感、总结要点才引入模型判断。执行层越确定整体越稳。第三层是编排层负责调度和组合技能。编排层决定当前任务该调哪个技能、多个技能之间按什么顺序串联、前一个技能的结果怎么传给下一个。这一层是 Agent 的主逻辑也是我在实际开发中改动最频繁的部分。2.3 接口设计决定了技能能长多大技能之间的接口如果不统一系统很快就会变成一团乱麻。我给所有技能定了几个强制约定输入必须是结构化的 JSON禁止传原始文本字符串输出必须是{ status: success | error, data: ..., error_msg: ... }的统一结构技能内部不得访问全局状态所有依赖都通过参数传入技能执行时间超过阈值必须返回超时标志不能无限等待。这些约定刚开始看起来很麻烦但正是它们让组合多个技能成为可能。我举个反面例子某个技能 A 的输出是表格 HTML技能 B 需要的是 Markdown 数组结果 B 接收后解析失败。根源就是 A 的输出没按约定结构返回。改了 A 的输出格式后B 一行代码没动就正常工作了。接口统一带来的另一个好处是可测试性。每个技能都能单独用一段模拟输入跑一遍不需要启动整个 Agent。我在项目里给每个技能配了一组测试用例每次改动代码后跑一遍能挡住大概七成的回归问题。3. 从零实现一个轻量技能管理框架3.1 技能描述与 Schema 设计我会用 YAML 来做技能定义因为可读性好也方便非开发人员参与编写描述。下面是一个技能定义的完整示例这个技能负责“从网页提取正文并生成摘要”id: web_article_summarizer name: 网页文章摘要 description: 输入一个网页 URL抓取正文内容并生成 200 字以内的中文摘要。适用于新闻、博客等长文页面。 tags: [web, summarization, zh] input_schema: type: object properties: url: type: string description: 文章的完整 URL max_sentences: type: integer default: 3 description: 摘要最多包含的句子数量 required: [url] output_schema: type: object properties: title: type: string description: 页面标题 summary: type: string description: 生成的中文摘要 source_word_count: type: integer description: 原文提取后的字数 execution: type: python_function module: skills.web_article_summarizer timeout_seconds: 30 retry_policy: max_retries: 2 backoff_seconds: 1有几个技巧值得说一下。description字段是我反复打磨的重点。一开始我写的是“抓取网页并总结”模型总在遇到 PDF 或视频页面时也调用它。改成“适用于新闻、博客等长文页面不适用于视频、PDF 或需要登录的页面”之后误调用率明显下降。模型本质上是通过描述来匹配任务的描述里的限定条件越明确匹配越精准。input_schema里的default字段也很重要。模型有时不会主动填所有参数有默认值兜底至少不会因为缺参数而调用失败。3.2 技能注册与加载机制我采用“目录扫描 自动注册”的方式来管理技能。项目结构大概是这样的skills/ __init__.py web_article_summarizer/ skill.yaml handler.py data_cleaner/ skill.yaml handler.py report_generator/ skill.yaml handler.py启动 Agent 时框架扫描skills目录下所有子目录逐个读取skill.yaml和handler.py构建一个技能注册表。核心代码如下import importlib.util import yaml from pathlib import Path SKILLS_DIR Path(skills) class SkillRegistry: def __init__(self): self._skills {} def load_all(self): for skill_dir in SKILLS_DIR.iterdir(): if not skill_dir.is_dir(): continue skill_path skill_dir / skill.yaml handler_path skill_dir / handler.py if not skill_path.exists() or not handler_path.exists(): continue with open(skill_path, r, encodingutf-8) as f: meta yaml.safe_load(f) handler self._import_handler(handler_path) self._skills[meta[id]] { meta: meta, handler: handler, } return self def _import_handler(self, path): spec importlib.util.spec_from_file_location( path.stem, path ) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module.execute def get(self, skill_id): return self._skills.get(skill_id) def list_skill_metas(self): return [v[meta] for v in self._skills.values()]把所有技能都放进注册表之后Agent 在系统提示词里会看到这样一段自动生成的技能列表可用技能 - web_article_summarizer: 输入 URL输出文章摘要。适用于新闻、博客长文。 - data_cleaner: 输入 CSV 路径输出清洗后的文件路径。适用于缺失值、格式修复。这段列表是动态生成的每新增一个技能Agent 自动就能“知道”。这样做的价值是技能可插拔上线新能力时加一个目录即可出问题要回滚时删掉目录或改版本号即可。你不需要在 prompt 里手工维护工具列表。3.3 调度与组合让 Agent 学会查手册注册只是第一步更关键的是让模型学会在合适的时机调用合适的技能。我在实现上参考了 OpenAI 的 function calling 思路但做了一层额外的“技能选择器”。整个流程是这样的模型接收用户请求模型从技能列表中挑选一个或多个技能输出调用请求JSON 格式包含技能 ID 和参数框架校验参数是否符合input_schema不合法就返回参数错误不直接执行框架执行对应 handler捕获超时和异常包装成统一结果返回给模型模型根据技能返回结果继续后续对话或调用下一个技能。这段流程听着不复杂但第 2 步里最容易出问题——模型选错技能。我在实盘中发现给模型的技能描述如果超过 15 个选择准确率会大幅下降。解决方法是给技能加tags在送入模型前做一次关键词预筛选。比如用户问“帮我把这个网页总结一下”我优先筛选web和summarization标签下的技能只把 3 到 5 个候选技能展示给模型准确率立刻回到 90% 以上。预筛选的代码大致是def filter_skills_for_query(query, registry, top_k5): scores [] for skill_id, entry in registry._skills.items(): meta entry[meta] score 0 for tag in meta.get(tags, []): if tag in query: score 1 score sum(1 for kw in meta[description].split(适用于)[-1:] if kw.strip() in query) scores.append((score, skill_id, meta)) scores.sort(reverseTrue, keylambda x: x[0]) return [s[1:] for s in scores[:top_k]]这个方案很轻量不需要引入向量检索但效果远好于把全部技能堆给模型。如果你的技能数量超过 30 个再考虑用嵌入式向量做语义召回也不迟。3.4 完整执行示例从技能选择到结果返回让我用一个完整的例子演示整个调用过程。假设用户发来一条消息“帮我看下这个网页里讲了啥https://example.com/intro”。框架收到消息后先执行预筛选匹配到web_article_summarizer技能把它以及另外两个可能相关的技能一并注入模型上下文。模型判断后生成如下调用请求{ skill_id: web_article_summarizer, arguments: { url: https://example.com/intro } }框架校验参数通过调用 handlerasync def execute(url, max_sentences3, **kwargs): try: html await fetch_page(url) text extract_main_text(html) summary generate_summary(text, max_sentences) return { status: success, data: { title: extract_title(html), summary: summary, source_word_count: len(text), }, } except Exception as e: return { status: error, error_msg: f页面抓取或摘要生成失败: {str(e)}, }执行完成后框架把结果作为一条工具消息返回给模型{ role: tool, skill_id: web_article_summarizer, result: { status: success, data: { title: Intro Page, summary: 这是一个介绍性质的页面..., source_word_count: 1200 } } }模型拿到结果后用自然语言组织成对用户的回复。整个过程模型只做“决策”——选什么技能、传什么参数、怎么根据结果回答——而真正的“执行”完全由代码控制避免了模型在抓取或解析过程中“自由发挥”。我在这个架构里最大的体会是代码负责稳定模型负责弹性。固定逻辑全部下沉到技能代码里模型只做那些真正需要理解和判断的部分。这样既保证了结果的可靠性又保留了模型的灵活性。4. 在真实项目里打磨技能的实操经验4.1 数据清洗项目里的技能沉淀理论说完了说说我在一个真实落地项目里的经验。这个项目的任务是做一个数据清洗 Agent输入是一个 CSV 文件输出是一份清洗报告和清洗后的文件。初始版本是把所有清洗规则都写进 system prompt结果模型经常漏掉某条规则或者把规则用错顺序。后来我把清洗过程拆成四个技能profile_data读取文件统计缺失率、类型分布、重复行输出数据画像deduplicate_rows基于指定的关键列去重clean_missing_values按列策略填充或删除缺失值generate_report汇总前面的操作记录生成清洗报告。这四个技能按照“先画像、再去重、再补缺失、最后出报告”的顺序串联。编排层用的是最简单的顺序执行上一个技能的输出作为下一个技能的输入之一。这套流程上线后效果立竿见影。以前模型会跳过画像直接清洗导致后面统计报告没数据可写现在顺序被技能系统锁死报告准确率从 72% 提升到 95%。这个提升主要来自任务边界清晰化——每个技能只需要管好自己那一小步模型不再需要在大上下文里追踪完整流程。4.2 参数设计与失败重试技能参数设计是我在打磨过程中踩坑最多的部分尤其是参数粒度问题。一开始我把参数设计得太细比如取出摘要技能我加了language、length_penalty、temperature这些参数。结果模型经常传错值不是因为模型笨而是参数太多、太专业模型根本不知道该怎么合理取值。后来我砍到只剩url和max_sentences两个业务参数调用失败率立刻降了下来。我的经验是不要暴露模型不理解的底层参数。像temperature这类模型参数应该由技能代码内部固定而不是开放给模型调。技能对模型暴露的参数必须是用户请求里自然存在的业务要素例如 URL、文件路径、列名、问题描述等。失败重试策略上我用的是经典的指数退避方案。第一次失败等 1 秒重试第二次失败等 2 秒最多重试 3 次。重试之间要确保技能内部没有副作用残留比如已经写入了一半的临时文件重试前需要清理。一个让我印象很深的坑某个技能调外部 API第一次调用因为网络超时失败但实际服务端已经处理了请求。重试后数据被重复提交产生脏数据。后来我给所有写操作类技能都加了幂等键——每次调用生成一个唯一 request_id服务端用这个 id 去重。这个改动彻底解决了重试带来的重复问题。4.3 技能评估怎么知道技能好不好使不评估技能质量就不知道改动是变好还是变坏。我用一套简单但有效的评估方法大概分三层第一层是单元评估对每个技能准备 10 到 20 条测试输入覆盖正常、边界、异常三类情况。比如摘要技能我会准备一篇正常文章、一篇超长文章、一个 404 页面。跑完看通过率低于 80% 的技能不进入系统。第二层是组合评估把技能按典型业务链路串起来跑一遍端到端任务。比如数据清洗项目里我用 5 组脏数据文件跑完全链路看最终报告是否完整、字段是否合理。这一层能发现单个技能正常但组合在一起的接口问题。第三层是线上抽样把实际用户行为中随机抽取的成功和失败案例记录下来每周复盘一次。我的做法是给每个技能调用都打日志记录技能 ID、输入摘要、输出状态、耗时然后用一个小脚本统计成功率随版本的波动曲线。这三层评估看起来投入大实际成本并不高。单元评估跑一次不到一分钟组合评估大概三分钟。但它帮我挡住了大量“感觉改好了、实际上改坏了”的回归问题。我最常说的一句话是没有评估体系的技能系统改代码全靠玄学。5. 常见问题与排查技巧5.1 技能调用失败的经典场景技能系统上线之后最常见的失败不是代码 bug而是模型没按预期调用技能或者技能参数传错。我整理了几类高频问题和对应的排查思路第一类是模型总是不调用技能直接尝试自己回答。这种情况几乎都是技能描述不够清晰模型没意识到当前任务属于某个技能。我解决的方法是给技能加“触发条件”描述例如“当用户提供 URL 并要求总结时必须调用本技能禁止直接根据 URL 的字符串内容编造答案。”加了这句模型就老实了。第二类是技能选择正确但参数传错。比如用户提到“把这份 CSV 里的重复行去掉”模型调用去重技能时把文件路径写成了用户原文里的文件名而这个文件名并不存在。排查思路是先看调用日志确认模型传入的参数值再回头调整描述明确指出“文件路径必须是实际存在的绝对路径不要凭空构造”。第三类是多技能串联时结果对不上。比如上个技能输出data是 JSON下个技能却当成纯文本处理。这种情况要去查两个技能的output_schema和input_schema是否匹配。我统一了所有技能的结果包装格式之后这类问题几乎绝迹。5.2 上下文污染与技能隔离技能执行时会产生中间日志、临时变量、工具错误信息这些内容如果不加控制地全塞进模型上下文会严重干扰后续推理。我的原则是默认只把技能的结果摘要和必要信息回传模型过程细节全部留在日志里。具体做法是给每个技能加上summarize_result方法把返回数据压缩成 2 到 3 行摘要。比如数据画像技能原始输出是一个包含 20 个统计指标的 JSON但回传给模型的摘要只有三句话“总行数 1044 行缺失值集中在 age 列占比 12%重复行 8 行。”模型有了这个摘要已经足够决定下一步操作没必要看 300 行原始统计。同时技能内部不要使用模型上下文存临时状态。我见过一个项目把上一步的清洗记录直接作为文本追加到 system prompt结果上下文越来越长模型越来越“糊涂”。技能之间的数据传递必须通过显式返回值而不是偷偷改上下文。5.3 技能冲突和优先级当一个动作可以由多个技能完成时模型多半会选错。比如“生成一份报告”既能用data_cleaner.generate_report也能用web_article_summarizer勉强凑一份模型就可能选到后者。我处理冲突的办法分三步。第一步给技能加priority字段同领域技能冲突时优先选高优先级。第二步在描述里明确写“本技能只处理 XX不要用它处理 YY”。第三步也是最狠的一步把明显不能胜任任务但容易被误选的技能从候选列表里剔除。在编排层我还引入了一个观察点模型反复在两个技能之间横跳时日志里会打出一条“犹豫”标记。这时我去看上下文通常是两个技能的 description 太相似或者用户请求有歧义。修复手段不是加更多提示词而是把两个技能的边界描述彻底分开。5.4 经典故障排查速查表下面这个表是我平时排查问题最常用的对照表看到什么现象就直接找对应根源现象可能原因排查重点模型不调用技能直接回答技能描述不清晰或触发条件缺失检查 description 是否写明了“必须调用”技能调用了但参数总错暴露了模型不理解的底层参数简化 input_schema只留业务字段技能选择正确但结果被模型扭曲回传内容太长模型丢失关键信息使用 summarize_result 压缩回传内容两个技能结果对不上输入输出 schema 不匹配检查统一包装结构确认字段路径同一操作重复执行产生脏数据缺少幂等机制给写操作技能加 request_id 去重技能超时拖垮整个 Agent未设置合理超时和重试给技能配 timeout 和 backoff 策略技能上线后旧任务性能下降新技能干扰了模型选择启用标签预筛选限制候选技能数量排查这一类问题我最大的心得是日志比 prompt 重要得多。给每个技能调用一行结构化日志记录下“模型选了什么、传了什么、技能返回了什么、花了多久”90% 的故障都能在日志里直接看穿。不要在模型输出上猜来猜去去查日志答案就在里面。6. 技能系统的后续扩展方向整套 agent-skills 体系搭建完成后我还做了一些扩展算是给这个系统的未来留了接口。第一个扩展是技能版本管理。每个技能目录里加一个version字段注册表里保留最近 3 个版本的 metadata模型仍然只用当前版本但随时可以切回旧版本。这个功能在小规模项目里看着多余一旦技能数量上了 20 个手动回滚就是必备能力。第二个扩展是技能之间的显式编排脚本。有些固定业务链路比如“调研 → 总结 → 生成周报 → 发送邮件”我不想让模型每一步都重新决策就用一个简单的编排脚本把技能串起来。脚本本身也是一个“超技能”暴露给模型的是“一键执行早报流程”这样的入口大幅减少模型的多步调用次数。第三个扩展是技能记忆。某些技能执行时依赖用户偏好比如摘要字数偏好、数据清洗时是否保留全角字符。我在技能内部加了一个轻量的偏好存储第一次执行时记录用户选择后续执行时自动沿用。当然这里要控制权限不能因为一次偏好就永久固化我给偏好记录加了一个“只保留最近 N 条”的窗口。第四个扩展是技能市场基座。当团队里多个项目都需要技能时我会把技能包发布到一个内部仓库其他项目直接拉取使用。因为技能本身是纯代码 纯配置天然适合版本化分发。这样可以不断沉淀团队经验避免“每个项目重写一遍清洗逻辑”的浪费。这些扩展不需要在第一天就全部搭好但设计时预留好接口后面加功能会顺手很多。核心就是把“技能”当成系统的一等公民能被注册、被调用、被回滚、被复用而不是深埋在 prompt 里的一段文字。我个人在实际操作中还有一个体会想分享所有技能框架的最终目标是让模型需要做的决策越来越少。如果你发现某个技能系统上线后模型还要费很大劲才能完成一个任务那大概率不是模型的问题而是技能切得不够细边界画得不够清楚。每当我遇到代码或者 prompt 改不动的情况我就会回头审视一次技能边界大多数时候问题会自己浮出来。希望这篇能帮你在自己的项目里少走几个来回。