做 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(city=city, date=date) 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],每行一个 dict,key 为列名 """ import sqlite3 if not query.strip().upper().startswith("SELECT"): raise ValueError("只允许 SELECT 查询") conn = sqlite3.connect("app.db", uri=True) 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 的“大脑”
技能写好之后,把它注册到 Agent:
from langchain.agents import create_structured_chat_agent tools = [search_docs, get_weather, run_sql] agent = create_structured_chat_agent( llm=llm, tools=tools, prompt=prompt, )到这里你会遇到第一个典型问题: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] name=search_docs args={"keyword":"本周进展","top_k":5} [SKILL_DONE] name=search_docs latency=0.312s result_summary="3 docs, topics=[会议纪要, 任务清单]"5.5 技能版本管理
最后聊一个容易被忽略的问题:技能的版本管理。你改了一个技能的逻辑,怎么知道它没破坏其他场景?我的做法是给每个技能维护一个version字段,在日志里输出,在注册表里记录。每次模型升级或技能改版,都跑一遍已有的测试用例集合,把回归结果和 version 对应起来。这样一旦线上出问题,能快速定位是模型升级导致的,还是技能改动导致的。
技能测试用例我推荐用问答对形式固化。比如搜索场景,写一组“用户问句 + 期望技能调用 + 期望参数”的用例,跑一个离线脚本验证。这比跑完整 Agent 流程快得多,也更容易自动化。
说实话,agent-skills 这套东西并不是什么高深算法,它更像是一种工程习惯:把能力当作一等公民来管理,让模型去思考而不是去瞎猜。自从按这个思路改造项目之后,我最大的感受是调试成本显著下降,团队成员也不再害怕接手 Agent 代码。即使现在模型还在快速迭代,技能这套抽象层依然稳定,它不会因为换一个基础模型而重写,这才是它真正值钱的地方。
最后再分享一个小技巧:每个技能上线之前,先单独写 10 到 20 条测试用例,固化在 tests 目录里。这部分投入会在后续每次模型升级、依赖库升级时加倍回报。别嫌麻烦,等线上 Agent 因为一个字段幻觉跑偏的时候,你会后悔当初为什么没多写几条。