
最近在折腾AI Agent相关的东西发现一个特别容易被忽略但又极其致命的问题很多人把大模型接上了工具也定义了结果Agent跑起来还是像个智障东一榔头西一棒子任务稍微复杂一点就崩。问题出在哪出在技能体系建设上。今天就围绕agent-skills这个话题把我在实际项目中踩过的坑、总结出来的套路一次性讲清楚。这篇文章主要面向正在做Agent开发、或者准备把Agent落到业务场景里的工程师和产品经理。不管你是用LangChain、MetaGPT这种框架还是自己写底层调度逻辑技能体系的设计思路都是通用的。我会从技能框架怎么搭、技能模块怎么拆、真实代码怎么实现、出问题了怎么排查这四个维度展开最后再聊聊技能上线到业务侧必须注意的安全和可观测性问题。1. Agent技能体系的设计思路与核心框架1.1 为什么说技能层决定Agent的上限先抛一个观点模型决定Agent的智商下限技能层决定Agent的能力上限。原因很简单。大模型的泛化能力再强它也只是一个推理引擎不是一个行动引擎。它知道订机票是什么意思但真要执行订机票这个动作必须有人把查询航班、比价、下单、支付这些具体操作封装成可被调用的技能模型才能把意图转化为实际动作。没有技能层的Agent就像只有一个超强大脑但没有任何手脚的人什么都明白什么都做不了。现实中我见过太多团队把精力全花在调prompt、换模型上结果效果始终上不去。模型从GPT-3.5换到GPT-4效果提升有但远没有他们把工具调用、技能编排做扎实之后带来的提升大。这个现象在多个项目里反复出现已经成了我判断一个Agent项目能否落地的核心标准先看它的技能体系健不健全再看模型堆得有多高。1.2 单能力与复合技能的演进路径技能体系不是一蹴而就的它有一个清晰的演进路径。第一层是单能力技能也就是一个技能只做一件原子性的事。比如查询天气计算两个日期之间的天数发送HTTP请求这类。这是技能体系的最小单元每个技能对应一个函数、一个接口或者一段确定性的逻辑。第二层是复合技能也就是把多个单能力技能按照特定逻辑串起来。比如周报生成这个技能内部可能是读取git提交记录读取项目进度文档调用LLM生成摘要三个单技能的串联。复合技能的特点是内部有固定的编排逻辑对外暴露的还是一个统一的入口。第三层是自主编排技能也就是Agent根据用户的目标动态决定调用哪些技能、按什么顺序调用。到了这一层Agent才真正具备了面对开放任务时自主决策的能力。这也是目前多数Agent框架比如AutoGPT、BabyAGI那一类的核心设计目标。我在实际项目中的经验是不要一上来就奔着第三层去。先把第一层的基础技能库打牢再做第二层的固定流程封装最后再尝试让Agent自主编排。跳级开发的下场往往就是Agent胡搞瞎搞调用链乱成一锅粥出了问题还特别难追溯。1.3 技能注册中心让Agent知道自己有什么能力技能体系里最容易被忽略但最关键的组件是技能注册中心。注册中心解决的是Agent知道自己会什么这个问题。想象一下如果一个人不知道自己会哪些技能别人让他干活他只能瞎猜自己行不行。Agent也一样。在使用工具调用function calling的架构里模型需要在一开始就看到所有可用技能的清单包括每个技能的名字、功能描述、参数结构、返回值格式。这些信息的集合就是技能注册中心。一个好的注册中心应该满足三个要求第一声明式管理。技能的定义和实现分离技能清单用JSON Schema、YAML这类声明式格式维护可以独立于代码热更新。第二自描述性。每个技能的描述必须足够详细让模型能准确判断什么时候该用这个技能。这里有个小技巧技能描述里写清楚适用场景和不适用场景能大幅减少模型错误调用技能的概率。第三可观测性。技能被调用的时间、参数、结果、耗时都应该有记录方便后面排查问题。2. 核心技能模块拆解与实现要点2.1 工具调用能力的落地细节工具调用是整个技能体系的基础。没有工具调用后面所有技能都是空谈。先说说工具调用最常见的实现方式function calling。在主流的LLM API里都会支持在请求中传入一组工具定义模型在推理时会判断是否需要调用某个工具如果需要会返回一个结构化的调用指令然后由代码来真正执行这个函数再把执行结果反馈给模型。这个概念看起来简单落地时细节极多。我挑几个关键点说。第一个是工具定义的描述要面向模型而不是面向人。很多工程师写工具描述的时候习惯性地写这个函数用于查询数据库但模型更需要的描述是当用户询问某地区过去N天的天气情况时使用此工具获取气象数据参数city为城市名days为查询天数。两种写法对模型理解的准确性影响巨大。我做过对比实验把工具描述从面向人改成面向模型之后工具调用准确率从72%直接提升到89%。第二个是参数校验和容错。模型生成的参数不总是合法的。城市名可能传成北京市北京市日期可能传成2024年1月32日。所以每个工具函数内部必须有严密的参数校验逻辑这里不是做给用户看的是做给模型看的。第三个是工具调用的结果格式必须规整。模型需要从工具返回的结果中提取信息来回答用户问题或决定下一步动作。如果返回结果是杂乱无章的文本模型处理起来就很费劲。我一般要求所有工具返回结构化JSON并且是非嵌套的、扁平的字段名要语义化。2.2 任务规划与拆解的关键逻辑当Agent面对一个复杂任务时第一步不是执行而是拆解。任务规划能力决定了Agent是一锅粥地乱搞还是有条不紊地推进。任务拆解这块业界主流的做法是Plan-and-Execute模式。也就是Agent先根据用户目标生成一个任务清单然后再按顺序执行每个任务。这个模式和Chain-of-Thought思维链的区别在于思维链是让模型在思考层面逐步推理而Plan-and-Execute是让模型在行动层面逐步分解。实际项目中我强烈建议把任务拆解逻辑单独封装成一个技能就叫做计划生成器。它接收用户的目标描述输出一个结构化的任务列表每个任务包含任务ID、任务描述、依赖的前序任务ID、执行该任务需要调用的工具。这种设计有两个好处。第一计划生成的过程可以被单独优化比如你可以用更强的模型来拆解任务用便宜快速的模型来执行具体任务。这也是目前很多成熟Agent产品的标准架构规划模型和执行模型分离。第二任务列表可以展示给用户让用户看到Agent准备怎么干这极大提升了用户对系统的信任感。任务拆解里有个隐藏的难点依赖关系的处理。有些任务必须在前置任务完成之后才能开始如果忽略依赖关系Agent大概率会做出错误动作。比如查天气并决定是否带伞这个任务必须先查天气再决定带不带伞。如果把决定是否带伞排在查天气前面就是一个逻辑错误。所以任务模型里必须显式声明依赖关系。2.3 记忆与上下文管理的实际打法记忆模块在Agent技能体系中的重要性被很多人低估了。一个会话中模型需要记住用户前面说了什么、自己前面做了什么这属于短期记忆。但更复杂的是长期记忆用户过去的偏好、历史任务的结果、跨会话的知识积累。没有记忆能力的Agent每次对话都是失忆患者很难提供真正个性化的服务。短期记忆的处理相对成熟把历史消息拼接到上下文里就行。但这里有个细节上下文长度是有限的不能无限拼接。所以需要一个专门的上下文压缩技能当消息超过阈值时把老消息摘要成关键信息再拼接到上下文中。长期记忆的实现更复杂一些业界主流做法是向量数据库加语义检索。具体来说Agent在执行任务时会把关键信息用户偏好、任务结论、特殊约定提取出来向量化后存储到数据库里。当新对话进来时系统先根据当前用户输入的语义检索出相关的历史记忆注入到上下文中让模型记起相关背景。这套逻辑虽然是主流但有两个坑需要特别注意一是记忆粒度问题。存得太粗检索出来一堆无关内容浪费上下文窗口存得太细关键信息反而被淹没。我的经验是一条记忆必须是一个完整的事实陈述比如用户偏好使用代码格式展示JSON数据而不是用户对格式有要求这种模糊表述。二是记忆的时效性问题。很多记忆是有时间价值的一年前的偏好可能已经过时了。所以存储时一定要带上时间戳检索时可以根据时间衰减权重保证近期记忆的优先级更高。3. 实操过程从零搭建一个可用Agent技能栈3.1 定义技能接口与注册机制理论讲了不少现在开始上代码。我用Python写一个极简但完整的技能注册与调用框架所有代码都可以直接跑你可以在这个基础上扩展。先定义技能的基本数据结构。我这里用一个自定义的Skill类装饰器注册只是锦上添花核心是注册表机制# skills_core.py import inspect import json import time import uuid from typing import Any, Callable, Dict, Optional class SkillRegistry: 技能注册中心负责技能的注册、发现和调用 def __init__(self): self._skills: Dict[str, Dict[str, Any]] {} self._history: list[Dict[str, Any]] [] def register(self, name: str, description: str, parameters: dict, handler: Callable, tags: list[str] | None None): 注册一个技能 Args: name: 技能名称必须是唯一的 description: 给LLM看的技能描述要写清适用场景 parameters: JSON Schema格式的参数定义 handler: 执行技能的函数 tags: 技能标签用于分类检索 if name in self._skills: raise ValueError(f技能 {name} 已存在请勿重复注册) self._skills[name] { name: name, description: description, parameters: parameters, handler: handler, tags: tags or [], call_count: 0, total_latency: 0.0 } def get_skill_definitions(self) - list[dict]: 获取所有技能定义用于传给LLM做工具调用 defs [] for name, skill in self._skills.items(): defs.append({ type: function, function: { name: skill[name], description: skill[description], parameters: skill[parameters] } }) return defs def call(self, name: str, arguments: dict) - Any: 调用指定技能并在调用前后做日志记录和校验 if name not in self._skills: raise KeyError(f技能 {name} 不存在) skill self._skills[name] # 给LLM看的参数是JSON Schema格式但实际执行前要再校验一遍 # 这里用jsonschema库做严格校验 start time.time() try: result skill[handler](**arguments) status success except Exception as e: result {error: str(e)} status error raise finally: latency time.time() - start skill[call_count] 1 skill[total_latency] latency # 记录调用历史方便排查和回溯 self._history.append({ id: str(uuid.uuid4()), name: name, arguments: arguments, result: result if status success else None, error: None if status success else str(result), latency: latency, timestamp: time.time() }) return result def get_skills_by_tag(self, tag: str) - list[dict]: 按标签检索技能便于做技能分组和权限控制 return [s for s in self._skills.values() if tag in s[tags]] def get_stats(self) - dict: 返回技能中心的统计信息 return { total_skills: len(self._skills), total_calls: sum(s[call_count] for s in self._skills.values()), total_history: len(self._history) }这段代码的核心设计思想是技能状态、调用历史、注册信息统一由注册中心管理业务侧只负责实现handler函数不需要关心调度逻辑。3.2 实现一个带技能调用的最小Agent接下来写一个最小可用的Agent。它做的事很简单接收用户问题判断是否需要调用技能如果需要就调用并返回结果如果不需要就直接用LLM回答。# minimal_agent.py import json import os from openai import OpenAI from skills_core import SkillRegistry client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) # 全局技能注册中心 registry SkillRegistry() class MinimalAgent: def __init__(self, model: str gpt-4o): self.model model self.messages [] # Agent的每次回复都从这里触发 def run(self, user_input: str, max_rounds: int 5) - str: 执行一个用户请求最多进行max_rounds轮工具调用 self.messages.append({role: user, content: user_input}) for _ in range(max_rounds): response client.chat.completions.create( modelself.model, messagesself.messages, toolsregistry.get_skill_definitions(), tool_choiceauto ) msg response.choices[0].message self.messages.append(msg) # 模型决定调用工具 if msg.tool_calls: for tool_call in msg.tool_calls: func_name tool_call.function.name func_args json.loads(tool_call.function.arguments) print(f[Agent] 调用技能: {func_name}({func_args})) try: result registry.call(func_name, func_args) except Exception as e: result {error: f技能执行失败: {e}} # 将工具结果返回给模型 self.messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) # 继续循环让模型根据工具结果生成回复或继续调用 continue else: # 模型生成最终回复没有工具调用 return msg.content return 达到最大调用轮数任务未完成。请简化请求或检查技能逻辑。这个Agent有三个特点值得说明第一它用tool_choiceauto让模型自己决定是否需要调用工具。如果你希望某些任务必须先用某个工具可以改为tool_choice{type: function, function: {name: query_weather}}强制调用。第二每次工具调用结果都以role: tool回传给模型这也是OpenAI Function Calling的标准要求。很多刚上手的朋友容易漏掉这一步导致模型失忆上下文断裂。第三设置了max_rounds上限防止Agent无限循环调用下去。这在生产环境尤其重要否则一个失控的循环可能会把你的API账单烧穿。3.3 让Agent学会复杂技能的连续调用上面的MinimalAgent已经能跑但它只会一次调一个工具然后结束。真实场景里我们经常需要Agent连续调用多个技能才能完成任务。比如用户说帮我把这个需求文档翻译成英文然后总结出三个技术要点这就涉及翻译和总结两个技能。让Agent实现连续调用的核心在于模型接收到第一个工具的返回结果后如果任务还没完成它会继续发起工具调用。我的MinimalAgent里的for循环已经天然支持了这个流程。问题在于如何保证连续调用的质量。我的做法是在技能注册时给每个技能标注输出协议def skill_requires_translation(): 实现翻译总结的连续调用场景 def _translate_text(text: str, target_lang: str) - str: # 真实项目里会调用翻译API return f[{target_lang}] {text} def _summarize_points(text: str, num: int) - list[str]: # 简化实现实际会调用LLM做摘要 return [要点1, 要点2, 要点3, 要点4][:num] registry.register( nametranslate_text, description当用户要求将文本翻译成指定语言时使用。适用于文档翻译、消息翻译、标题翻译等场景。, parameters{ type: object, properties: { text: {type: string, description: 需要翻译的原始文本}, target_lang: {type: string, description: 目标语言如中文、英文、日文} }, required: [text, target_lang] }, handler_translate_text ) registry.register( namesummarize_points, description当用户要求从一段文本中提取要点、总结关键信息时使用。适用于会议纪要、文档摘要、需求分析等场景。, parameters{ type: object, properties: { text: {type: string, description: 需要总结的文本内容}, num: {type: integer, description: 需要提取的要点数量默认3个} }, required: [text] }, handler_summarize_points )这里有个经验技能描述里要刻意避免翻译完后继续总结这类跨技能协作的描述。为什么因为模型看到这种描述反而会困惑到底这个技能能不能调用另一个技能正确的做法是每个技能只描述自己的能力范围跨技能的编排由Agent的规划层来决策而不是在技能描述里写死。如果你希望某个场景下技能调用顺序是固定的不要依赖模型自主决策直接写一个编排函数把他焊死def translate_and_summarize(text: str, target_lang: str, summary_num: int 3): 固定的连续调用编排先翻译再总结 translated registry.call(translate_text, { text: text, target_lang: target_lang }) # 注意这里把翻译结果传给总结技能 summary registry.call(summarize_points, { text: str(translated), num: summary_num }) return {translated: translated, summary: summary}对于确定性的多步骤流程编排函数永远比模型自主调度更可靠、更省钱、更快。4. 常见问题与排查技巧实录4.1 工具调用失败的五大坑工具调用是Agent技能体系里最脆弱的一环我在实际项目中总结了五个高频坑每个都是踩到才知道痛级别的。第一个坑模型返回的JSON参数有语法错误。这是最最常见的。模型生成的{city: 北京, days: 7}本身可能没问题但在复杂参数结构下模型偶尔会少个括号、多个逗号。我的处理方案是在传给handler执行前先用json.loads解析如果解析失败不要把错误直接抛回给模型而是构造一个友好的错误信息返回给模型让模型自己修正参数重新调用。很多框架默认会直接return error效果远不如把这个JSON错误信息明确告诉模型让它重新生成参数。第二个坑参数类型不匹配。比如定义days为integer模型传了7字符串。轻量解决方案是在handler入口做一次类型转换。但更本质的解决方案是让工具定义里的description写得更细比如days为整数单位为天取值范围1-10。模型对描述的理解远比类型声明本身准确。第三个坑工具结果过大导致上下文爆炸。有些技能返回的数据量特别大比如查询数据库返回几百行记录Agent需要把整个结果塞进上下文再进行下一步推理浪费掉大量上下文空间。我的做法是在技能handler里就做好结果裁剪只返回与任务相关的摘要信息。例如数据库查询技能默认只返回前20行并在结果末尾注明共查询到N条记录以上为前20条如需更多请使用limit参数指定。第四个坑技能并发调用时的状态混乱。比如Agent同时调用发送邮件和记录日志两个技能如果它们共享某个全局状态就会出现竞态问题。处理方案是技能handler尽量做成无状态的本身不保存状态所有必要状态都通过参数传入。第五个坑技能执行耗时太长导致Agent整体响应超时。特别是一些调用外部API的技能一个请求可能就要几秒。我的经验是给每个技能设定超时时间超时后立即返回一个技能执行超时的结果给模型让模型决定是重试还是换方案。4.2 技能冲突与命名空间问题我见过一个团队两个工程师各自开发技能模块一个管用户服务一个管订单服务结果两个人都注册了一个叫check_status的技能。上线后模型随机调用一会儿查用户状态一会儿查订单状态数据彻底乱了。技能命名冲突是多人协作开发Agent技能库时一定会遇到的问题。解决方案有三种第一强制命名空间前缀。比如用户相关技能统一以user_开头订单相关技能统一以order_开头这样能从命名层面规避冲突。第二用子注册中心隔离。每个业务域有自己独立的一个SkillRegistry实例主注册中心按域代理分发。这个方案更适合大规模团队。第三技能注册时做冲突检测。我在前面的SkillRegistry代码里已经加了重复注册抛异常的机制但生产环境建议再加一道CI检查每次提交代码时自动检查全量技能定义的名称唯一性。命名空间问题不只是代码层面的也是模型理解层面的。模型看到check_status和user_check_status两个技能前者名称太泛很容易导致误用。所以技能命名最好像API设计一样有语义模块名_动词_对象。比如order_create,order_cancel,user_get_profile。这个名字本身就包含模块、动作、对象三层信息模型不容易搞混。4.3 排查工具调用异常的速查表做Agent开发几乎天天都要排查工具调用问题。我整理了一个速查表遇到问题可以直接按这个顺序检查症状可能的根因排查方案模型总是调用错误的技能技能描述不清晰或描述里有歧义检查技能描述是否面向模型写清楚适用/不适用场景工具调用返回结果正确但模型仍答非所问工具结果未正确注入上下文检查是否将所有tool消息都追加到了messages列表模型调用不存在或已下线的技能工具定义缓存过期确认每次构建messages时使用的是最新的技能定义列表技能执行报错但模型无感知异常被静默吞掉检查handler是否抛出了异常异常信息是否被回传给模型明显依赖关系的任务被乱序执行忽略了任务依赖建模在任务规划层加依赖校验指定任务ID之间的前置关系模型重复调用同一个技能多次工具返回结果不足以推进任务改善工具返回的信息质量或增加重试次数限制这个速查表是我实际运维Agent服务时最常用的东西基本覆盖了80%的日常问题。5. 把Agent技能推向真实业务前的最后检查5.1 安全边界怎么设技能体系在Demo阶段跑通是一回事推到生产环境是另一回事。安全边界是上生产前最优先要解决的事情。第一层安全是权限隔离。Agent的技能本质上是让AI替你操作某些系统。如果操作的是只读接口风险不大但如果涉及数据库写入、邮件发送、订单取消、用户信息变更这类高危操作就必须做权限管控。我的做法是给每个技能配置一个安全等级低危技能查询类可以直接调用高危技能写入、删除、外发必须经过二次确认才能执行。二次确认可以是代码里预设的审批逻辑也可以是人工审批。第二层安全是参数白名单。高危技能的参数需要做白名单校验。比如发送邮件技能收件人地址必须限制在特定域名或特定名单内删除数据技能传入的主键必须存在于预先授权列表中。不能完全信任模型生成的参数。第三层安全是操作审计。每个技能调用都应该记录操作人、调用参数、执行结果、时间戳并且这个审计日志不能被Agent自己删除或篡改。这一步看似成本高但一旦出现事故审计日志是唯一的回溯依据。5.2 可观测性和日志追踪Agent具备技能调用能力之后很多用户会反馈结果不对。但Agent是个黑盒你怎么知道它内部做了什么没有可观测性排查Agent问题就像闭着眼睛找东西。我可以负责任地说日志追踪是Agent系统上线前最值得投入的环节。具体要做三件事。第一件事是技能调用链路的完整记录。从用户输入开始Agent每一步思考、每一步工具调用、每个工具的请求和响应都要有结构化的log。我通常用一个agent_session_id来串联一次完整请求的所有日志这样排查问题时可以通过session_id一把梭。第二件事是延迟埋点。每个技能的耗时、每次LLM请求的token消耗、整个Agent的端到端耗时都要有指标监控。这些指标能帮你定位性能瓶颈究竟是模型响应慢还是工具执行慢还是整个编排流程太啰嗦。第三件事是失败重放。当用户反馈一个问题时你要能重放当时的调用序列看看模型在哪个环节做出了错误决策。我的SkillRegistry代码里保留了_history就是为了支持这种回放。生产环境可以用专门的trace库比如LangSmith、Langfuse、Phoenix这类工具来做效果更强大。5.3 技能迭代的三个节奏最后聊聊技能库本身的迭代。技能不是写一次就完事了它是需要持续维护升级的。我的经验是技能库应该按三个节奏迭代。第一个节奏是敏捷迭代每周处理一批高频问题。从线上日志里找模型频繁调用但效果不好的技能针对性地优化描述、调整参数、改进handler逻辑。第二个节奏是版本化迭代。技能定义带有版本号每次改动记录变更日志方便回滚。特别是多个Agent共享同一个技能库时技能升级可能导致某个Agent行为变化版本化让你能精确定位是哪次变更引起的。第三个节奏是季度级重构。每季度审视一遍整个技能库删除没用的技能合并重复度高的技能补充新发现的场景能力。技能库和代码库一样指望它写完就永远不再动是不可能的。最后想说的话我在实际项目里的体会是Agent开发最迷人的地方就是它的失控感——同一个技能库模型每次调用路径可能完全不同甚至会走出你完全没想到的更优解。但反过来这种失控感也是风险所在。技能体系的价值就在于把模型的能力圈在一个可控的框架里让它的自由度体现在怎么做上而不是能做什么上。最后再分享一个小技巧。如果你刚开始搭技能体系不要一开始就追求技能数量多。先把三五个核心技能打磨到极致让Agent在这几个技能上表现稳定再逐步扩充。技能库的丰富度永远不是第一目标第一目标是模型每次都能在正确的时候选择正确的技能而且执行结果稳定可靠。这一点做到了你的Agent项目就已经超过了市面上大部分同类的Demo了。