☰
从零构建Agent技能体系:大模型工具调用的完整工程实践
2026/10/14 9:16:43 网站建设 项目流程

1. 从“智能问答”到“可执行技能”:Agent 的定位变化

这两年做大模型应用,一个明显感受是:纯聊天的玩意儿越来越没有壁垒,真正有价值的是让模型去“干活”。而“干活”这件事,落到工程层面就是 agent 的技能体系——agent-skills。你可以把它理解为给大模型配了一套“可执行的工具箱”:识别用户意图、拆解任务、挑选工具、组织参数、执行动作、核对结果,一整套链路下来,它才从“会说话”变成“会办事”。

我最早接触 agent 是在某个内部模拟项目里,当时团队的目标是让系统能基于用户的一句话自动完成数据查询、报表生成、消息通知三条链路。最初我们想做的是把每个功能打成一个 API,让模型自己选。但真的跑起来才发现,API 只是最底层的东西,模型怎么知道什么时候该调、参数怎么填、结果怎么判断成功失败,这些才是核心。而 agent-skills 要解决的,恰恰是这一层问题。

如果你正在做知识库问答、自动化助手、企业流程机器人这类项目,或者已经在用大模型 API 开发应用,那么这篇内容会比较对路。我不会去讲各种炫酷的 Agent 框架怎么用,而是从最朴素的角度拆解一套技能体系是怎么从零搭起来的。

1.1 Agent 到底是什么,为什么“技能”这么关键

一个 agent 可以理解成一个循环系统:观察(Observe)→ 决策(Decide)→ 行动(Act)→ 再观察。以 LLM 为“大脑”,每轮循环它接收新的环境信息,决定下一步动作,然后调用某个工具执行,工具返回结果后再交给模型判断下一步。

这个过程中,模型自身并不直接操作外部系统,它只能通过“技能”间接控制。技能就是一段可以被 LLM 按声明调用的可执行能力,比如“查询订单状态”“发送告警消息”“解析日志文件”。技能设计得好不好,直接决定了 agent 的真实可用度。

举一个很现实的例子:同样一套工具集,A 方案把每个工具做成一个独立函数,让模型自己看函数名猜参数,结果模型经常把字符串传成整数,或者把“订单号”和“用户ID”搞混;B 方案为每个工具写了详细的描述、参数类型、约束条件和成功标准,模型调用准确率从六成直接升到九成以上。差异就在技能描述的质量上。

1.2 技能不是功能列表,而是对外部世界的操作边界

很多人一上来就把所有接口全部暴露给模型,觉得“工具越多就越强”,其实这是个误区。技能是你的 agent 与外部世界之间的受控边界,它既要给模型足够的操作空间,又要把危险动作、权限范围卡死。

我在一个模拟项目里曾经踩过这样的坑:把“数据库执行任意 SQL”做成一个技能丢给模型,结果模型为了完成一个简单的“查一下用户数”的请求,生成了DROP TABLE一类的高风险语句,虽然被数据库权限拦住了,但整个流程已经非常不可控。后来我们把技能粒度细化成“查询用户数”“查询最近订单”“更新订单状态”这种语义明确的操作,并且每个技能内部做了参数校验和白名单约束,agent 的稳定性才真正上来。

所以你可以把 agent-skills 理解为:一批经过语义封装、带权限边界、带参数约束、带结果校验的原子能力。它们的组合构成了 agent 解决复杂问题的基本盘。

2. Agent-skills 的前置准备:环境、模型与框架选型

在动手写技能之前,先要把地基打好。我自己的经验是这部分如果不重视,后面排错能排到怀疑人生。下面按几个关键点展开。

2.1 运行环境与基础设施

我的常规配置是 Python 3.11+,虚拟环境跑 agent 核心进程。依赖上不追求重型框架,核心只有几样:

组件用途备注
LLM SDK调用大模型接口兼容 OpenAI 协议即可,便于切换国产/开源模型
日志框架记录每轮决策和调用明细建议使用结构化 JSON 日志
异步任务队列处理耗时工具调用实际生产建议用工作队列,避免阻塞主循环
向量存储(可选)技能检索 / 知识召回技能数量大时用得上

这里有个容易忽略的点:技能执行环境要和模型调用环境隔离。模型调用是网络 I/O,技能执行可能是本机命令行、数据库查询、内部 HTTP API,如果混在同一个进程里,一个技能卡住会把整个 agent 卡死。我一般把重型技能放到独立进程或者独立服务里,agent 只通过协议调用。

2.2 模型选择与上下文预算

Agent 技能系统对模型的要求和普通问答完全不是一个维度。普通问答模型只要“理解力”强就行,agent 场景还要求指令遵循能力和工具调用稳定性。实际测下来,不同模型在同样一套技能描述下表现差距非常大,有的模型连“必须返回 JSON”这种命令都会偶尔忽略。

上下文预算也是个大问题。每个技能描述动辄几百字,注册二十个技能就是上万字上下文。模型每轮都要重新读一遍技能清单,不仅浪费 token,还容易让模型在长上下文中分心,出现“忘了用某个技能”“把技能 A 的参数套到技能 B 上”这类问题。

我的做法是把技能描述拆分两级:

  • 总览层:简短列出有哪些技能,一句话说明用途,控制在几百字内;
  • 明细层:模型决定调用某个技能时,再动态注入该技能的完整参数描述。

这样既保证了模型知道“有哪些牌可用”,又不会让它一次消化太多冗余信息。之前做工具集成时踩过这个坑,总以为描述越全越好,结果上下文一长调用准确率反而降,拆成两级之后改善明显。

2.3 工具协议与返回格式规范

技能最终要落地执行,就绕不开“模型怎么调、工具怎么回”这个协议问题。目前常见的有两种思路,一种是用模型厂商自带的 function calling/tool use 机制,由框架解析模型输出的结构化工具调用参数;另一种是把“工具调用”本身也还原成普通文本输出,自己在代码里做解析。

我的建议是:优先使用模型支持的 function calling 机制,因为这会显著提升参数解析的准确率。早期做某图像处理 Demo 时我用过文本解析方案,模型输出稍有格式偏差就崩,正则写了一堆还是补不完,换成原生 function calling 之后几乎不用处理解析异常了,省了很多心力。

工具返回也必须有约定。我在协议里强制要求每个技能返回结构化 JSON 字符串,统一包含success、result、error三个字段。这样 agent 主循环看返回结果时逻辑非常简单:success=true就把result给模型继续思考,success=false就把error喂给模型让它重试或换技能。这套约定看着简单,实际排错时帮了大忙。

3. 技能拆解:从自然语言意图到原子操作

技能不是随便把一堆函数堆在一起,它需要经过一层“意图翻译”:用户说一句话,系统判断需要哪些操作,然后按正确顺序编排。我在做技能拆解时遵循几个原则。

3.1 从用户旅程反推技能组合

不要从现有接口出发,要从用户任务出发。假设场景是“员工自助查询报销进度”,用户路径大概是:登录 → 查单号 → 看审批节点 → 对于被驳回的申请查看原因。

这套流程落地成技能,需要的不是“报销接口查询模块”这种大而全的东西,而是:

  • 解析报销单号(从用户对话中提取单号)
  • 获取报销单状态(按单号查询数据库/API)
  • 获取审批节点明细(列出审批人和状态)
  • 生成驳回原因摘要(把非结构化备注整理成摘要)

每个技能保持“单一职责”,组合起来就能覆盖完整流程。这样做还有个额外好处:每一个技能都可以单独测试,出了问题定位极快。如果做成一个大技能“查询报销全流程”,参数又会膨胀,模型调用时漏传参数的情况会明显增多。

3.2 技能声明:一个技能就是一个完整的说明文档

每个技能不是只有一段代码,还需要一份“使用说明书”,主要包含:

字段作用示例
name技能唯一标识order_query
description给模型看的自然语言描述,说明该技能解决什么问题、何时使用查询订单当前状态,适用于用户询问物流或订单进度时
parameters参数名、类型、是否必填、取值范围、示例值order_id: string, required
cost_hint技能开销提示(耗时/资源消耗),帮助模型排优先级high_latency
success_condition什么结果算成功,便于 agent 判断是否完成目标返回订单状态节点列表,每个节点含时间戳
error_message失败时返回的错误信息模板订单号不存在,非重试类错误

每个字段都很重要。我尤其想说description和success_condition——前者是给模型决策用的,决定它会不会在正确时机调用;后者是给 agent 循环用的,决定它会不会反复横跳或误报成功。

3.3 技能之间的依赖与顺序编排

有些任务必须按特定顺序执行,比如先user_auth再order_query。这种顺序关系我一般不在技能内部写死,而是通过“状态机”来处理:agent 主循环维护一个session_state,记录当前已满足的前置条件,只有状态达标时才允许调用某些技能。

这种设计比在技能内部校验权限优雅得多——模型只需要按声明做事,规则在外部约束,代理不会因为上下文上下文污染而跳过关键步骤。

4. 核心实现:一个最小可复制的技能调度器

说了一堆理论,下面给一个可以自己跑起来的最小实现。这里用兼容 OpenAI 协议的接口实现,很多内置框架调用方式类似,但核心思路是一样的,你完全可以照着自己改写。

4.1 技能注册与元数据管理

首先定义一个技能类,每个技能就是一个BaseSkill子类。

# skills/base.py from pydantic import BaseModel, Field class SkillResult(BaseModel): success: bool result: str = "" error: str = "" class BaseSkill: name: str = "" description: str = "" parameters: list[dict] = [] required: list[str] = [] async def execute(self, **kwargs) -> SkillResult: raise NotImplementedError

然后写两个具体技能,一个模拟订单查询,一个模拟发送通知:

# skills/order_skill.py from skills.base import BaseSkill, SkillResult class OrderQuerySkill(BaseSkill): name = "order_query" description = "查询订单当前状态,当用户询问物流、发货、签收进度时使用,输入订单号即可。" parameters = [ {"name": "order_id", "type": "string", "description": "用户提供的订单号"} ] required = ["order_id"] async def execute(self, **kwargs) -> SkillResult: order_id = kwargs.get("order_id") if not order_id or len(order_id) < 8: return SkillResult(success=False, error="order_id不合法") # 模拟查询 return SkillResult(success=True, result="订单已签收,签收时间 2024-12-30 14:22")

注册中心也很简单,一个字典就够了:

# registry.py from skills.order_skill import OrderQuerySkill SKILL_REGISTRY = { "order_query": OrderQuerySkill(), } def get_skill(name: str): return SKILL_REGISTRY.get(name)

如果技能数量多了,可以改成自动扫描skills/目录加载,避免注册表越写越长。我这里故意保留最简版本,方便看逻辑。

4.2 主循环:模型决策与技能调用的衔接

主循环是 agent 的心脏,负责把“决策”和“执行”串联起来。核心伪代码如下:

# agent_core.py import json from openai import OpenAI client = OpenAI() def build_tool_schemas(): schemas = [] for skill in SKILL_REGISTRY.values(): schemas.append({ "type": "function", "function": { "name": skill.name, "description": skill.description, "parameters": { "type": "object", "properties": { p["name"]: {"type": p["type"], "description": p["description"]} for p in skill.parameters }, "required": skill.required, } } }) return schemas async def run_agent(user_message: str, max_rounds: int = 5) -> str: messages = [{"role": "user", "content": user_message}] context = {"history": [], "tools": build_tool_schemas()} for _ in range(max_rounds): resp = client.chat.completions.create( model="your-llm-model", messages=messages, tools=context["tools"], tool_choice="auto", ) choice = resp.choices[0].message if not choice.tool_calls: # 没有调用任何工具,说明任务收敛 return choice.content or "任务完成" messages.append(choice) for call in choice.tool_calls: skill_name = call.function.name skill = get_skill(skill_name) args = json.loads(call.function.arguments) result = await skill.execute(**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result.model_dump_json(), }) return "达到最大轮数,任务未收敛"

这个循环大家应该能看明白:模型返回工具调用请求 → 执行对应技能 → 把结构化结果返回给模型 → 模型继续推导或给出最终答案。它不依赖任何重型 Agent 框架,全文加起来不到一百行,却已经具备了一个 agent 的基本骨架。

4.3 失败恢复:让 agent 自己“发现自己错了”

光有主循环还不够,agent 必须有失败恢复能力,否则一次参数传错就会彻底卡壳。我在实践中的做法是把异常当作“反馈”重新交给模型,让它自己判断是换参数重试、换技能,还是停止并求助。

举例:模型调用order_query传了order_id="123",明显非法,技能返回{"success": false, "error": "order_id不合法"}。主循环把这段错误返回给模型,模型看到后会自动更正成完整订单号再查一次。这个能力靠的是“模型能读懂错误信息”,所以错误信息一定要写得语义化,不能只返回“500”或者“参数错误”这种没头没尾的东西。

再一个容易踩的坑是循环重试陷阱。如果某个技能多次失败,模型可能原地打转,不断用同样的错误参数重试。我加了一个简单保护:同一个技能连续失败超过两次,就把错误信息标记为“已尝试多次,请换一种处理方式或请求用户补充信息”,模型通常会改走其他路径。

5. 上下文提示工程设计:让模型在正确时机选择正确技能

技能系统的问题,十有七八出在“模型选错了技能”或“参数填错”。这两个问题都可以靠提示工程大幅缓解。

5.1 系统提示要告诉模型“你是谁、你能做什么、你该遵守什么”

一个典型的系统提示我会写成这样:

你是一个任务执行助手,负责理解用户需求并调用可用的技能完成任务。 可用技能如下: - order_query: 查询订单状态。仅当用户提供完整订单号时可用。 - notify_user: 通过站内信/短信通知用户。仅当用户明确要求通知时使用。 你必须遵守的规则: 1. 如果用户没有提供必要参数,先请求补充,不要猜测。 2. 技能调用失败时,仔细阅读错误信息并合理重试。 3. 当无法确定调用哪个技能时,明确告知用户当前可提供的帮助。 4. 使用工具后,基于工具结果组织自然语言回复,不要虚构未包含的信息。

好的系统提示能把模型的“试错”成本降下来。虽然没有绝对保证,但我观察过,加了这些明确规则后模型在边界场景下的表现会稳定不少。

5.2 技能描述要像“给同事解释任务”一样写

前面提到过技能描述的重要性,这里重点说怎么写。我总结的公式是:

触发场景 + 输入条件 + 输出形态 + 限制条件

举个例子,一个垃圾描述是“查询订单”,合格的描述是“查询订单当前状态,适用于用户询问物流进度、发货时间、签收情况时使用;必须传入完整订单号(以字母开头、至少8位);返回结果为订单各状态节点的时间线”。模型看到这段描述后,几乎不会在不相关场景乱调用。

参数描述同理,不能只写“订单号”,要写“订单号,用户短信中收到的以字母开头的一串编号,如 AB12345678”。这类细节直接影响参数抽取正确率。

5.3 防止模型“脑补”工具返回结果

这是 agent 项目里最容易被人忽视、危害却最大的一类问题:模型明明没有调用工具,却“根据经验”直接回答了用户。典型场景是用户问“我的订单到哪了”,模型根本不调order_query,直接编了个“您的订单已发货”糊弄过去。

我之前一直觉得大模型不会有这种低级问题,直到实际测试次数多了才发现这其实是结构化 agent 交互中真实存在的风险。后来加了硬性策略:

  • 凡是涉及需要实时数据的技能,在系统提示中强制声明“必须调用工具后方可回答”;
  • 如果模型没有调用任何工具就直接给出结论,但任务明显需要实时数据,主循环会判定为“未完成任务”,回退重试或要求模型补调工具。

这招虽然粗暴,但实测很有效,基本能杜绝“凭空捏造状态”的问题。

6. 测试与评估:如何确认技能体系是真的“可用”

很多团队做完 agent 就上线,然后靠用户骂了才知道哪里不行。我的经验是:agent 技能系统必须有独立的评测集和持续回归机制,否则改一行代码可能不知道破坏了什么。

6.1 构造评测用例:覆盖常规、边界与对抗场景

我会把测试用例分成几类:

类型例子期望行为
常规用例“帮我查订单 AB12345678 的状态”调用 order_query,返回真实状态
缺参用例“帮我查一下订单状态”(没有单号)不调用技能,请求用户补充单号
边界用例订单号是“abc”调用技能失败后,模型识别参数非法并让用户更正
歧义用例“我那个东西发了没”模型应该询问具体是哪个订单,而不是瞎猜
危险用例“把订单删了”无对应技能,模型明确拒绝或告知无权限

每个用例都定义好输入、期望动作、期望最终回复三个维度。跑评测时记录:技能选择是否正确、参数抽取是否准确、流程次数是否过于冗余、最终回复是否合理。

6.2 参数抽取准确率是核心指标

我从实践中得到的最有用的评估指标是工具调用级准确率,就是模型生成的每次工具调用中,技能名和参数都正确的比例。这个指标比“最终任务成功率”好拆解得多,出了问题可以精确定位到底是“模型选错技能”还是“参数抽错”。

我自己的经验阈值是:常规场景下工具调用级准确率至少要做到 95% 以上,否则 agent 根本没法上线。低于这个数,你会被各种“重试-失败-重试”的负循环拖死。

6.3 回归测试要自动化

技能系统改动太频繁了,改一个描述、换一个模型,都可能让整套表现大变。我会在 CI 里放一组精简回归用例,每次提交自动跑一遍。不需要多,三五十条覆盖各类型就足够,跑一次几分钟,但能拦住七八成回归问题。

这里也提一下,评测不是越难越好。一开始我把用例写得过复杂,agent 怎么都过不了,后来发现是模型本身能力边界问题,不是提示工程能解决的。所以评测集要分层:一层验证基础能力,一层挑战上限,分层看待结果,进步才看得清。

7. 真实运行环境的踩坑记录

框架搭好只是开始,真正磨人的是接入真实环境之后的一堆破事。这里挑几个我印象最深的坑。

7.1 工具返回结果超出上下文窗口

第一个大坑:某个技能返回了巨大的 JSON 数据,比如用户订单详情关联了几十条物流轨迹、退换货记录、支付快照,一次性全塞给模型,直接超长截断或者把模型“淹没”在一堆无关字段里,导致后续决策质量急剧下降。

解决思路是技能内部做结果裁剪。每个技能可以配置返回摘要策略:把复杂结果先汇总成几百字的精简摘要再返回给模型,完整的细节数据存入临时存储,用户深入追问时再用另一个技能查明细。这让整个系统的 token 成本也同步下降。

7.2 参数类型与枚举值错位

第二个常见问题是参数类型对不上:模型把start_date: "2024-01-01"传成了"20240101"或者把枚举值传错。这类问题不像缺参那样会被技能拦住,往往是执行到一半才发现。

我的对策是在技能参数声明里用 JSON Schema 的enum、format字段严格约束,同时开启strict模式(部分开放接口已经支持)。如果模型强行传非法值,技能执行器统一返回“参数类型不合法,请参考参数说明重新传入”,引导模型自己修正。实测开启严格校验后,参数类错误明显减少。

7.3 并发与幂等的血泪教训

第三个坑是真实并发场景。多个用户同时请求时,如果技能内部操作了共享状态(比如文件、数据库游标、内存计数器),会出现互相覆盖的情况。我遇到过一次:两个任务同时写同一个临时文件,结果互相把内容冲掉了,agent 却以为操作成功。

后续我定的规矩是:

  • 技能必须是无状态的,所有状态放外部存储;
  • 涉及写操作按 key 加分布式锁,保证同一资源的操作串行化;
  • 每个技能在重试时要保证幂等(重复执行结果一致)。

这三条做下来,并发问题基本绝迹。做 agent 技能如果你不打算只玩 Demo,这三条早晚都躲不掉。

8. 从技能列表到技能生态:后续可以这样扩展

最后聊点后续演进的方向,也是我踩完一轮坑之后最想回头重构的部分。

第一,技能积累多了之后,检索会成大问题。比如你有上百个技能,模型不可能每次都读完所有描述,这时就需要一个技能检索器:先用用户消息在向量库中召回最相关的 Top K 个技能,只把它们的描述注入给模型。这个思路和 RAG 很像,但检索的对象是技能描述而非文档,效果取决于描述文本的质量。我在某个模拟项目里试过,技能数量超过五六十个之后这种方案几乎是必需品。

第二,技能需要版本管理与灰度发布。技能本质上是独立部署的服务,修改时要考虑兼容性和回滚。我建议给每个技能加版本号,主循环记录调用日志时一并落版本。性能回退时能用版本快速回滚,而不是连代码一起回滚。

第三,观察模型在技能调用中的行为模式。高频使用哪些技能、哪些技能组合常被同时调用、哪些技能经常失败,这些数据汇总后能反向优化技能设计。比如发现两个技能经常配对被调用,就可以考虑合成一个复合技能,降低调用次数和上下文消耗。

我目前在做的一些新尝试,是把技能从“执行动作”升级为“带状态的流程节点”,比如一个技能可以在多个轮次间保持中间状态(等待用户确认、等待异步任务完成)。这种设计一旦跑通,agent 能处理的就不是单轮查询,而是完整的多轮工作流了。

如果你也在构建自己的 agent 技能库,记住一条主线:技能是给模型用的,不是给自己用的。所有设计决策——描述怎么写、参数怎么定、错误怎么报——都应该从“模型能不能读懂并正确执行”这个角度出发。顺着这条线走,你会发现 agent 从“偶尔灵验”到“稳定可用”之间,并没有那么远的距离。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询