
做 AI Agent 的开发者尤其是刚接触 LangChain、CrewAI 或者 AutoGPT 这类框架的人应该都会碰到一个原型很漂亮、一到真实业务就崩的尴尬期。问题往往不在模型的智能程度而在于你给 Agent 配的那把“工具包”——这里的工具包在社区里有个更专业的叫法agent-skills也就是智能体技能模块。agent-skills 不是什么大而全的“万能插件”而是你把 Agent 需要执行的动作拆成一个个边界清晰、可独立评估、可复用的技能单元。比如“搜索本地文件”“调用某个 API 拿数据”“生成一张图表”每个单元都自带输入输出协议、错误处理和日志记录。Agent 拿到用户请求后先做任务拆解再按需从技能库里调度这些技能组合完成工作。这篇文章能帮到的读者很明确正在做智能客服、自动化脚本、个人知识库助手或者只是想给 ChatGPT / 本地大模型套一层自己业务能力的人。我会尽量不堆概念直接讲技能体系怎么设计、怎么实现、怎么踩坑给你一套可以直接抄作业的实操方案。1. 先搞清楚agent-skills 到底解决什么问题1.1 从“万事通变身”说起先抛一个很多人刚入坑时的直觉大模型已经这么聪明了为什么还要给 Agent 单独搞一套 skills直接写 prompt 告诉它“你会用 Python 处理 Excel”不就行了吗实测过的人都懂prompt 只能解决“会什么”的表达问题解决不了“能拿到什么”“操作是否有权限”“失败怎么处理”这些执行层的问题。你告诉模型“你会调用公司内部的人力系统 API”但模型没有真实接口的凭证不清楚字段命名规则也不知道接口限流策略。它只能对着空气编一个调用方式最后堆出一段看似合理但根本跑不起来的伪代码。更麻烦的是模型在生成过程中一旦出现字段幻觉会自动把不存在的 ID 或日期补全导致下游系统收到一堆脏数据。这个问题我在早期项目里反复遇到过后来才意识到根子不在模型能力而是技能封装不到位。skills 的存在就是把“模型会说”和“工具能做”之间的裂缝焊死。每个技能都像一个独立小服务有明确的触发条件、参数校验、超时机制、重试策略。模型在规划阶段只需要回答“用哪个技能”至于这个技能背后是爬虫、数据库查询还是脚本执行模型不需要也不应该关心。所以说agent-skills 第一个价值是执行边界。它把 LLM 从“必须了解每个工具细节”的负担里解放出来让模型只负责决策把执行交给经过测试的代码。这个分工一旦清晰你的 Agent 稳定性会有质的提升。1.2 为什么不能把全部逻辑写死有人会接着问既然技能最终也是代码那我直接把流程用代码写死不就行了何必绕一圈让 Agent 来调度这个问题我踩过很深的坑。早期做自动化任务时我把业务流程全部写成了 Python 脚本用户需求一变就要改代码。比如原来要求“每日拉取订单并汇总”后来变成“只汇总华东区订单并且按品类分组”就得改一遍流程逻辑。每改一次测试一次非常痛苦。后来接入大模型让 Agent 走决策路径我发现一个关键收益技能本身的“语义”开始对用户可见。你可以在技能清单里用自然语言描述“search_docs搜索本地知识库中与主题相关的文档支持按时间范围过滤”这样 Agent 在规划时会把它当作一个“可用能力”来思考而不是黑盒函数。换句话说agent-skills 的核心价值不是“让代码能跑”而是“让能力和意图对齐”。代码是给机器读的技能描述是给模型读的。它既要满足机器可执行的硬约束又要满足模型可理解的语义约束。这才是这套体系真正难的地方也是这篇文章想重点拆解的部分。从工程角度看技能模块化还带来一个额外好处可测试性。一个技能可以单独写单元测试、做回归验证而不用等整套 Agent 流程跑通。你甚至可以在技能层做灰度发布先让 10% 的流量走新技能实现观察指标再全量切换。这在传统大杂烩脚本里基本不可能实现。2. 技能体系的设计思路先把“能力边界”画清楚2.1 技能拆分的最小颗粒度设计 skills 第一步不是写代码而是画边界。我会用三个问题来考验每一个候选技能这个技能能不能用一句话说清楚它“输入什么、输出什么”它的失败点是可预期的吗会不会因为外部依赖变化而不可用同一个技能被复用在不同场景时有没有违背单一职责举例一个叫“fetch_stock_price”的技能输入股票代码输出当前价格、涨跌幅、成交量的 JSON。它清晰、独立、可测试。而一个叫“stock_analysis”的技能既要做数据抓取又要做技术指标计算还要生成文字报告它就不满足单一职责应当拆成 fetch_stock_price、calc_indicators、generate_analysis 三个技能。这样做的好处很明显每个技能可以单独测试、单独替换实现、单独计费审计。而且 Agent 在规划时如果发现单靠一个技能完不成任务它会自然而然地去组合多个技能而不是在同一个技能内部写一堆超长分支。我见过很多人一上来就写一个“万能技能”把所有工具封装进去结果模型面对的是一个巨大的函数签名参数几十个描述几百字根本不知道该怎么选。这种技能从设计上就输了因为它的能力边界是模糊的。请记住技能越小越容易被模型正确选择技能描述越具体越容易被正确编排。2.2 组合与编排技能不是孤岛单技能只是积木组合才是 Agent 的灵魂。但组合起来之后新的问题来了谁来负责编排顺序调用顺序错了怎么办中间技能失败是整体回滚还是部分重试我的经验是不要把编排逻辑全部压在模型身上。虽然 LangChain 这类框架允许 Agent 自主决定 tool 调用顺序但现实业务通常需要一定的“硬约束”。比如“先查库后写报告”这种流程我建议把编排节点做成一个轻量 DAG有向无环图配置在技能层之上用 YAML 描述节点依赖、重试上限、超时时间。举个例子nodes: - id: load_docs skill: search_docs retries: 2 - id: extract_entities skill: ner_extract depends_on: [load_docs] - id: gen_report skill: generate_report depends_on: [extract_entities]这种“技能自治 流程显式编排”的混合模式在真实项目里比纯靠 Agent 自由发挥稳定得多。Agent 的价值在于处理“哪些技能需要组合”的意图理解而流程引擎的价值在于保证“一旦决定组合执行顺序可控”。还有一个细节技能之间尽量设计成无状态。无状态意味着技能可以任意组合不用担心上下文污染。如果某个技能必须依赖前置技能的输出最好的方式不是让它去内部调用另一个技能而是通过外部编排把前置输出作为它的输入参数传进去。这样每个技能仍然是一个黑盒测试和维护都简单。2.3 技能描述怎么写模型才听得懂既然技能描述是给模型读的它的写法就很有讲究。我的模板是四段式功能一句话、参数说明、返回值说明、不适用场景。以 get_weather 为例获取指定城市未来几天的天气情况返回温度、风力、降水概率。 参数 - city: 城市名称必填如“北京”“上海” - date: 日期可选默认当天格式 YYYY-MM-DD 返回 - dict包含 temperature, wind, precipitation 三个字段 不适用场景 - 不要用本技能查询历史天气30天前 - 不要用本技能查询空气质量最后那两句“不适用场景”特别重要。模型看了之后会减少误用尤其是当多个技能描述较相似时这种负向约束能明显提升选择准确率。实测下来加了这段之后技能误调用率能下降三分之二左右。3. 从零搭一套 agent-skills实操步骤与踩坑记录3.1 框架选型和环境准备目前常见的 agent-skills 落地方式有三类一是直接用 LangChain / LlamaIndex 这类框架的 Tool 机制二是自己写一套基于 JSON Schema 的函数注册表三是用微调后的模型做“技能路由”。我的建议是如果你还在验证阶段直接用 LangChain 的 tool 装饰器最省事。它底层帮你处理了函数参数 schema 提取、错误消息返回、token 消耗统计这些事让你能专心打磨技能本身的逻辑。等你的技能数量超过二三十个、需要权限管理和灰度发布时再考虑自研注册表不迟。环境准备上推荐 Python 3.10、LangChain 0.1.x以及一个支持 function calling 的模型。OpenAI 的能用但如果你对数据隐私有要求本地通过 vLLM 部署 Qwen 或者用智谱的 GLM 系列也完全可行。不要一上来就引入重型框架先让三五个核心技能跑通再逐步扩展。提示不要在一开始同时引入多个 skill 管理插件先用最朴素的工具注册方式把全链路打通再考虑治理问题。否则你会被层层封装搞到怀疑人生。3.2 三个通用技能的原型实现我在第一次搭 agent-skills 时写了三个通用技能search_docs、get_weather、run_sql。它们覆盖了文本、外部API、数据库三种典型场景用来验证整套体系的通用性很合适。search_docs 核心逻辑from langchain.tools import tool tool def search_docs(keyword: str, top_k: int 3) - list[str]: 搜索本地知识文档中与关键词相关的段落返回按相关度排序的文本列表。 参数 - keyword: 搜索关键词必填 - top_k: 返回结果数量默认3 返回 - list[str]每个元素是一段相关文档内容 import os import sqlite3 # 使用本地倒排索引缓存避免每次全量扫描 conn sqlite3.connect(doc_index.db) # 简单实现分词后查 sqlite FTS5 索引按 bm25 排序 rows conn.execute( SELECT content FROM docs WHERE docs MATCH ? ORDER BY rank LIMIT ?, (keyword, top_k) ).fetchall() conn.close() return [r[0] for r in rows if r[0]]get_weather 核心逻辑tool def get_weather(city: str, date: str today) - dict: 获取指定城市未来几天的天气情况返回温度、风力、降水概率。 参数 - city: 城市名称必填如“北京”“上海” - date: 日期可选默认当天格式 YYYY-MM-DD 返回 - dict包含 temperature, wind, precipitation 三个字段 # 这里接入实际的天气 API比如和风天气 # 需要注意API 返回字段一定要裁剪不要整个 JSON 全抛给模型 raw weather_api.get(citycity, datedate) return { temperature: raw[temp], wind: raw[wind_dir], precipitation: raw[precip], }run_sql 核心逻辑tool def run_sql(query: str, max_rows: int 50) - list[dict]: 在只读数据库连接上执行 SELECT 查询返回最多 max_rows 行结果。 参数 - query: SELECT 语句只能是只读查询 - max_rows: 最多返回行数默认50 返回 - list[dict]每行一个 dictkey 为列名 import sqlite3 if not query.strip().upper().startswith(SELECT): raise ValueError(只允许 SELECT 查询) conn sqlite3.connect(app.db, uriTrue) cursor conn.execute(query[:200]) # 限制 SQL 长度 columns [d[0] for d in cursor.description] rows cursor.fetchmany(max_rows) conn.close() return [dict(zip(columns, row)) for row in rows]三个技能的实现都很朴素但已经涵盖了技能设计的关键点参数校验、结果裁剪、安全约束。尤其是 run_sql限制 SQL 长度和只读检查这两步在生产环境里能挡住一大波低级事故。3.3 把技能挂进 Agent 的“大脑”技能写好之后把它注册到 Agentfrom langchain.agents import create_structured_chat_agent tools [search_docs, get_weather, run_sql] agent create_structured_chat_agent( llmllm, toolstools, promptprompt, )到这里你会遇到第一个典型问题prompt 怎么写才能让 Agent 正确调用工具我的经验是不要在 system prompt 里写“你可以使用以下工具”这种废话而是给出一段带示例的说明。比如当用户询问天气时应优先调用 get_weather 获取实时数据而不是根据知识库猜测。 当用户要求查找项目相关文档时应使用 search_docs不要自行编造文件内容。这种指令的作用是建立“问题类型 → 技能”的映射直觉。模型看到用户问“明天上海冷不冷”会自然想到 get_weather而不会去调 run_sql。没有这种映射模型在多个技能之间犹豫决策 token 消耗会成倍增加响应速度明显变慢。还有一个容易踩的点tool 装饰器会把函数的 docstring 作为技能描述所以 docstring 里不要写废话要用斜体或加粗标出参数和返回。很多框架还会截断特别长的 docstring建议控制在 300 字以内。4. 真实场景演练让技能跑起来4.1 场景自动整理周报现在看一个完整的业务场景用户说“帮我整理本周的项目进展周报”。整个链路是这样的Agent 接收用户请求Agent 调用 search_docs 搜索本周会议纪要和任务更新文档Agent 调用 run_sql 查询本周各模块的 issue 关闭数量Agent 汇总数据后生成 Markdown 周报这个场景看起来简单实际上有一个非常隐蔽的坑search_docs 如果只按关键词模糊匹配会经常搜到无关文档。比如搜索“本周进展”可能匹配到上周甚至上个月的文档。我建议在技能内部增加一个“相关性阈值”低于阈值的匹配直接丢弃避免脏数据进入报告。具体做法是在技能内部对返回结果做个 score 判断低于 0.4 的不返回。别小看这一步它能显著提升最终报告的质量。模型看到的相关内容越准确生成出来总结的幻觉就越少。周报里如果出现一条过期的 bug 记录业务方立刻会对整个系统失去信任。另外注意 run_sql 返回的行数限制。周报场景里只需要 “每个模块关闭了多少 issue”一个简单的 group by 就能搞定结果集很小。但如果查询条件写得太宽泛可能会拉回几万行。我在 run_sql 里默认只取 50 行避免模型一次性面对太多数据导致上下文爆掉。4.2 场景API 对接与数据提取第二个场景更常见对接第三方订单系统把订单 JSON 里的字段提取成结构化数据。很多人让模型直接解析 JSON 字段结果模型出现幻觉把不存在的字段名编进去。我踩过这个坑客户发来一个订单报文模型解读出 “customer_ref_no” 这个字段实际后端根本没有这个字段最终导致下游对账全部对不上。更稳妥的做法是技能内部用显式 mapping 函数把 JSON 转换为固定 schema模型只负责传参不负责看字段。比如订单 JSON 里的 “buyer_name”在内部统一转成 “customer_name”这些映射规则写在代码里而不是依赖模型去推断。这个思路可以推广到所有场景凡是确定性映射的工作尽量下沉到技能内部完成凡是意图判断的工作才留给模型做。你越早明白这条原则Agent 在生产环境里的表现就会越稳定。模型擅长的是“理解用户到底要什么”而不是“准确地把 A 字段映射成 B 字段”后者交给代码又快又不会出错。4.3 技能调用的上下文管理跑几个真实场景之后你会发现Agent 和技能之间的上下文传递会逐渐变大。比如 search_docs 返回五段文本run_sql 返回二十行记录这些都会进入对话历史下一轮对话又继续累加。上下文一长不仅费用飙升模型还会出现“注意力漂移”开始关注前面无关的细节导致后续工具调用参数越来越少。我的解决办法是给每个技能增加一个“输出摘要器”。技能返回完整结果给执行引擎的同时返回一个摘要给模型。比如 search_docs 返回完整段落给下游做 RAG但给模型的信息只有“共检索到 3 条相关文档主题分别是 A、B、C”。这样模型有足够信息做下一步决策而不会把大量原始文本塞进上下文。这个“双通道返回”设计是我自认为整个技能体系里性价比最高的一个改进。它不改变技能逻辑只调整返回结构就能让上下文长度下降 60% 以上任务完成率反而更高。5. 常见问题与排查技巧实录5.1 技能调用失败但报错不明显现象Agent 返回“抱歉我无法完成该任务”但日志里没有任何异常堆栈。 原因技能抛出的异常被框架吞掉转成了 Agent 回复。排查在技能内部加 try/except把错误信息显式返回给模型并且在技能描述里写明“如果遇到 xxx 错误请提示用户检查数据源”。比如 get_weather 里 API 超时了我可以返回一个 dict 带上error: weather_api_timeout这样模型至少能感知到“天气服务临时不可用”回复时会给用户一个合理预期而不是一句空泛道歉。实际编码时我常写一个小装饰器def safe_skill(func): tool functools.wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: return {error: f{func.__name__}_failed: {str(e)[:200]}} return wrapper5.2 技能执行太慢 / 内存耗尽现象run_sql 查询大表时把内存吃满。 解决在技能内部强制加 LIMIT把 max_rows 默认值设置得很小同时对慢查询设置 timeout。我见过一个案例模型生成了一条没有 WHERE 条件的全表扫描语句结果将整个分析库拖垮。加上 query 长度限制比如只允许前 200 字符后这种事故彻底消失。此外我给所有技能统一加了超时控制。Python 里可以直接在技能函数外面包一层concurrent.futures的超时机制超过 10 秒直接返回超时错误。这能防止单个技能的异常拖垮整个 Agent 流程。5.3 多技能互相干扰现象同一个 Agent 同时挂载了 8 个技能后模型经常选错。比如用户问“今天股票行情”它跑去调了 “search_docs”。 原因技能总描述太接近模型分不清边界。解决规范每个技能的 description统一加前缀例如“【搜索类】”“【数据类】”同时在 description 中明确写“不适用场景”告诉模型什么时候不要用。还有一个办法是给技能加权重标签高频使用的技能在描述里多说相关词低频技能收敛描述减少误导。如果你发现某个技能几乎从不被正确调用试试把它从 Agent 的 tools 列表里移出去改用子 Agent 内部持有。这样主 Agent 面对的工具列表更短选择准确率自然提升。5.4 排查三件套参数、摘要、耗时我建议每个技能至少输出三份日志信息调用参数、返回摘要、耗时统计。平时排错突然多了这一步你大概率能定位问题所在。参数日志能告诉你模型是不是传错了参返回摘要能告诉你模型拿到的信息是不是过时耗时统计能暴露性能瓶颈。具体日志形式我用很朴素的 JSON 行直接打到 stdout采集起来也方便。每个技能入口打一条出口打一条中间异常再打一条。这个“三件套”帮我解决了至少 80% 的线上排查问题。你可以完全照搬[SKILL_CALL] namesearch_docs args{keyword:本周进展,top_k:5} [SKILL_DONE] namesearch_docs latency0.312s result_summary3 docs, topics[会议纪要, 任务清单]5.5 技能版本管理最后聊一个容易被忽略的问题技能的版本管理。你改了一个技能的逻辑怎么知道它没破坏其他场景我的做法是给每个技能维护一个version字段在日志里输出在注册表里记录。每次模型升级或技能改版都跑一遍已有的测试用例集合把回归结果和 version 对应起来。这样一旦线上出问题能快速定位是模型升级导致的还是技能改动导致的。技能测试用例我推荐用问答对形式固化。比如搜索场景写一组“用户问句 期望技能调用 期望参数”的用例跑一个离线脚本验证。这比跑完整 Agent 流程快得多也更容易自动化。说实话agent-skills 这套东西并不是什么高深算法它更像是一种工程习惯把能力当作一等公民来管理让模型去思考而不是去瞎猜。自从按这个思路改造项目之后我最大的感受是调试成本显著下降团队成员也不再害怕接手 Agent 代码。即使现在模型还在快速迭代技能这套抽象层依然稳定它不会因为换一个基础模型而重写这才是它真正值钱的地方。最后再分享一个小技巧每个技能上线之前先单独写 10 到 20 条测试用例固化在 tests 目录里。这部分投入会在后续每次模型升级、依赖库升级时加倍回报。别嫌麻烦等线上 Agent 因为一个字段幻觉跑偏的时候你会后悔当初为什么没多写几条。