AI Agent技能体系设计:从Prompt拆解到Function Calling落地的完整框架

发布时间:2026/10/21 23:22:40
AI Agent技能体系设计:从Prompt拆解到Function Calling落地的完整框架 做 AI Agent 方向的人应该都有同感大模型本身的推理能力越来越强但真正让 Agent 落地干活的反而是那些看起来不起眼的技能模块——查订单、改地址、读文档、调接口、算价格……这些能力如果不组织好Agent 就只是一个会聊天的空壳。这两年我一直在折腾 Agent 相关的东西最深的体会是技能体系的组织方式直接决定一个 Agent 是“能真用”还是“只能演示”。“agent-skills”这个项目就是围绕这件事展开的。它想做的不复杂把模型需要的各种操作能力从 prompt 里拆出来做成有标准接口、有元信息、可注册、可调用、可单独测试的模块。这篇文章我不谈空概念直接讲我为什么这么设计、踩过哪些坑、以及一套可以照着落地的代码结构。适合正在做 Agent、准备做 Agent、或者手头堆了一堆 function 不知道该怎么管理的同学。1. 为什么要给 Agent 搭一套技能体系1.1 把能力写进 prompt是最省事也最坑的方案最早做 Demo 的时候我习惯把所有工具能力直接写进系统提示词里比如“你是一个客服助手你可以查询订单、可以修改地址、可以申请退款”每个操作再给一段说明和参数例子。刚开始只有两三个能力的时候效果还行。等能力加到十几个问题就来了。首先是 token 成本。每加一个技能就要往 prompt 里塞一段描述十几个技能下来光提示词就几千字每次请求都在付这些钱。其次是注意力稀释模型面对一大段工具说明经常漏看、误读尤其是两个功能相近的技能放在一起模型就开始瞎选。最要命的是维护成本改一个参数得去翻 prompt 里的描述改完还要担心其他地方有没有引用。后来做某个跨平台系统的时候我彻底放弃了“全塞 prompt”的写法。原因很现实技能会越来越多prompt 是会膨胀的而代码是可以拆分的。把每个能力做成独立模块让模型按需发现、按需调用才是能长期维护的方案这也是“agent-skills”这个项目最初的出发点。1.2 技能体系的三块基石一套能用的技能体系我个人拆成三块技能定义、注册中心、调度链路。技能定义负责回答“这个技能是什么、能干什么、怎么调用”。它不只是一个函数还包括名字、描述、参数结构、执行逻辑、以及使用边界。注册中心负责管理所有技能让它们可以被发现、被枚举、被校验而不是散落在各个文件里。调度链路负责在模型需要某个能力时把合适的技能送上去等模型交出参数后执行再把结果回传给模型。这三块里最容易做砸的是调度链路。很多人以为把技能列表丢给模型就完事了但其实模型的调用成功率非常依赖你喂给它的描述质量、参数约束、以及出错后的兜底逻辑。后面我会把每一块都展开讲先记住一个结论技能体系不是一个工具函数仓库它是一个有治理机制的运行系统。2. 技能设计怎么让模型不把它用错2.1 一个技能就是一个最小闭环技能设计的第一个原则我总结为“最小闭环”。什么意思一个技能应该能独立完成一件完整的事而不是只做半件事。举个例子。做客服 Agent 的时候我最初把“查询订单”拆成了两个技能一个负责根据订单号调接口一个负责把结果格式化。结果模型经常只调第一个拿到一堆 JSON 就原样丢给用户用户看到的是{status: delivered, items: [...]}这种鬼东西。后来我把两个合并成一个“查询订单详情”技能内部做完查询、格式化、空值处理直接返回一段能读的话问题就消失了。同样的道理适用于所有技能技能内部要处理输入校验、业务逻辑、异常兜底、输出格式化。对外暴露的边界尽可能薄让模型只关心“传对参数、拿结果”不要指望它会帮你做数据清洗和格式整理。这不是偷懒是因为模型在“后处理”这件事上极不稳定与其让模型自由发挥不如在代码里把结果锁死。2.2 描述比代码更值钱做技能体系之后我才发现判断一个技能好不好用不取决于它背后的代码多复杂而取决于你写给模型看的那段描述。模型不是通过读源码来理解技能的它只能通过描述和参数结构来猜测。我早期的描述写得很偷懒比如“查询订单信息”没了。结果就是用户问“我的东西发货了吗”模型不会去调这个技能因为“发货了吗”和“查询订单信息”之间的关联不够直白。后来我把描述重写成“查询订单详情。当用户询问订单当前状态、物流进度、商品明细、金额明细时使用此技能。输入订单号或用户手机号输出订单状态、商品列表、物流节点和金额。若订单不存在返回明确提示。”改完之后调用准确率肉眼可见地上去了。这里的门道是描述里要包含触发场景的关键词物流、明细、状态、输入实体的说明订单号还是手机号、以及输出内容概览。你在描述里写得越像人话模型越容易把它和用户的真实意图对上。2.3 边界写清楚负面条件也写清楚描述里除了写“什么时候用”我建议还要写“什么时候别用”。这个发现是我在调试日志里看到的用户只是闲聊一句“听说你们家发货挺慢”模型居然调用了查询订单技能因为没有订单号可用技能直接报错整个对话体验被破坏。后来我在每个技能描述里都加了负面条件。比如查询订单技能会写明“仅当用户明确提供订单号或可识别身份的手机号时使用如果用户只是在抱怨发货慢不需要查询具体订单请直接回复安抚内容”。加了这段之后误调用率降了一大半。负面条件的价值在于模型做工具选择时本质是在做分类决策。正面描述帮它区分“这个技能负责什么”负面描述帮它排除“这个技能不负责什么”。两个都写清楚了分类边界才足够清晰。尤其是那些职责相邻的技能比如“查询订单”和“修改订单”一定要互相写清楚边界否则模型很容易把修改操作错接到查询技能上。2.4 参数描述和约束是最后一道防线很多技能代码本身没问题问题出在参数上。模型传进来的参数跟人犯的错一样类型不对、格式不对、关键字段缺了。你永远不能指望模型全知全能它就是会搞错日期格式把字符串传进整数参数甚至把空值传进来。所以技能设计里参数这一环必须有两个手段第一在参数描述里写清楚格式和取值比如“date 格式为 YYYY-MM-DD”就比“日期”两个字靠谱得多第二在代码侧做严格校验不符合直接返回结构化报错不要让脏数据流进业务逻辑。描述是让模型尽量传对校验是保证它就算传错也不会炸。两者缺一不可。3. 从零落地一套可用的 skill 框架3.1 技能的数据结构讲完设计直接上代码。这是我后来整理出的一套比较稳定的技能数据结构用 Python 的话大概是这样的from dataclasses import dataclass, field from typing import Any, Callable, Dict, Optional dataclass class Skill: name: str description: str parameters: Dict[str, Any] handler: Callable[..., Any] when_to_use: str when_not_to_use: str output_format: str text confirm_before_execute: bool False tags: list[str] field(default_factorylist)每个字段的意思很直白name技能的唯一标识格式建议用“动词名词”比如query_order、update_address方便在日志里检索。description给模型看的主描述一句话讲清技能作用等价于第 2 节说的“什么时候用”。parametersJSON Schema 格式的参数结构直接对接大模型的 function calling 接口。handler真正的执行函数参数解析完之后由它干活。when_to_use / when_not_to_use补充使用边界会拼进最终的技能描述里。output_format声明输出类型是给上层 Agent 看的决定结果要不要二次处理。confirm_before_execute标记这个技能是否有副作用比如改地址、退款这类操作需要先跟用户确认。为什么要用 dataclass 而不是裸函数因为这套结构是给运行系统看的注册中心要遍历所有技能、要校验参数、要生成给模型的描述列表统一的结构才能让这些逻辑复用。裸函数虽然写完就跑但一旦技能多起来就会变成一盘散沙。3.2 注册中心与自动发现技能定义好了需要一个地方把它们统一管起来。我习惯做一个注册中心核心逻辑是技能列表的维护和查询class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} def register(self, skill: Skill): if skill.name in self._skills: raise ValueError(fduplicated skill name: {skill.name}) self._skills[skill.name] skill def get(self, name: str) - Optional[Skill]: return self._skills.get(name) def list_skills(self) - list[Skill]: return list(self._skills.values()) def build_descriptions(self) - list[dict]: descs [] for s in self._skills.values(): d { type: function, function: { name: s.name, description: s.description, }, } if s.when_to_use: d[function][description] f 触发场景{s.when_to_use}。 if s.when_not_to_use: d[function][description] f 不要使用此技能的场景{s.when_not_to_use}。 if s.parameters: d[function][parameters] s.parameters descs.append(d) return descs这里的build_descriptions是我反复调整过的关键方法。它把技能里的所有元信息拼成给模型看的函数描述列表包括触发场景和负面条件。每次调用之前系统只需要传这份列表给模型模型就能知道现在有哪些工具可用。注册方式我后来也改成自动扫描了。因为技能文件一多手动注册总有遗漏。现在我在项目里约定每个技能文件必须暴露一个SKILL变量启动时扫描目录自动注册新技能只要按模板写文件就能被系统发现不需要改任何注册代码。这对团队协作特别友好A同学加技能不会干扰B同学改逻辑。3.3 接进大模型的调用链路技能体系要发挥作用最终得接进模型的调用链路里。这里我以大多数大模型都支持的 function calling 流程为例核心循环大概是这样的def run_agent(registry: SkillRegistry, user_message: str): messages [ {role: system, content: 你是客服助手...}, {role: user, content: user_message}, ] for _ in range(MAX_STEPS): tools registry.build_descriptions() response llm.chat(messagesmessages, toolstools) if not response.tool_calls: # 模型没有调用技能直接返回最终回复 return response.content # 模型请求调用一个或多个技能 for call in response.tool_calls: skill registry.get(call.function.name) try: result execute_skill(skill, call.function.arguments) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse), }) except SkillError as e: messages.append({ role: tool, tool_call_id: call.id, content: f技能执行失败{e.message}请根据错误调整参数或告知用户。, }) return 多次调用仍未完成已终止。execute_skill做的是参数解析和校验def execute_skill(skill: Skill, arguments_json: str) - dict: if skill.confirm_before_execute: # 副作用技能必须先经过用户确认不能直接执行 return {require_confirmation: True, skill: skill.name} args json.loads(arguments_json) # 这里可以用 JSON Schema 校验库也可以用手写校验逻辑 validate_arguments(skill.parameters, args) result skill.handler(**args) return {result: result}这里面有几个细节我特意做了处理。第一异常不能裸抛要包装成SkillError这样模型收到的是一条可读的错误信息而不是一堆堆栈模型才知道下一步该怎么补救。第二副作用技能在执行前要返回一个“需要确认”的信号由上层对话逻辑向用户确认后再执行而不是模型说改地址就真的改了。第三每次模型调用工具后的结果都要追加进 message 历史模型才能基于结果继续推理。3.4 技能描述动态裁剪技能一多又会遇到新问题一次请求把所有技能描述都发给模型token 一样会爆炸。比如我维护了 50 个技能光描述就有几千 token里面绝大多数跟当前用户问题无关。后期我加了一层轻量级的技能预筛选。先用一个简单的规则或者小模型根据用户消息和对话历史从注册中心里召回 Top 几 个候选技能只把候选技能的描述发给大模型。这就像先做一轮粗筛再让主模型精调。实测下来调用准确率没有下降token 开销省了 40% 以上。这里要注意的是预筛选召回是要尽可能高的宁可多召回几个也不要把真需要的技能漏掉。所以我做的是偏保守的召回关键词命中、标签命中、历史调用记录命中都会计入分数只把明显无关的技能筛掉。如果你对召回策略没把握最稳妥的做法是暂时不做裁剪先把准确率跑稳再优化成本。4. 上线之后反复踩的坑4.1 模型选错技能最先该检查描述选错技能这个问题我在真实环境里遇到太多次了。排查的时候先别急着怀疑模型能力多数情况是描述写得不够清楚。我做过的经典错误是把“查询优惠券”和“领取优惠券”两个技能放一起用户问“我有多少张券”模型去调了领取技能导致重复发券。排查方法是翻日志。把模型每一次调用哪个技能、传入什么参数、用户原话是什么记录下来。看多了你就能找出规律当模型在一个技能和另一个技能之间犹豫时,基本都是描述中的触发词没有覆盖用户表达习惯。比如用户说“我的券还能用吗”你的描述里只写了“当用户查询优惠券数量时”它当然识别不出来。把描述改成覆盖更多口语表达准确率很快就能提上来。4.2 参数校验做得好能省一半事故有一段时间我的技能执行总是报错仔细一看全是参数问题日期传了中文“今天”数字传了字符串手机号带上了区号。这些模型都会干得出来。后来我立了个规矩任何技能的 parameters 必须写严格 JSON Schema枚举值必须列全格式必须加正则约束。举例来说{ type: object, properties: { order_id: { type: string, description: 订单号纯数字例如 202501150001, pattern: ^[0-9]{12}$ }, action: { type: string, enum: [cancel, confirm, delay] } }, required: [order_id, action] }加了这个约束之后模型乱传参数的频率显著降低。即便偶尔传错服务端校验也会直接挡回去不会让错误参数影响业务。很多人觉得这些工作是边际优化但一套成熟的 Agent 系统靠的就是这些“防御性细节”撑起来的细节越严线上事故越少。4.3 技能内部报错绝不能裸抛技能执行报错有两种情况一种是业务上确实查不到数据另一种是代码本身有 bug。这两种情况对模型来说意义完全不同所以在错误信息上必须区分开。业务查不到数据要返回的是“查无此订单请核对订单号是否正确”这类信息模型看到后会引导用户重试。代码 bug要返回的是包含错误代码的提示让模型没法瞎编解释并把详细堆栈记录到日志里供人排查。我封装了一套技能异常体系分为BusinessError和InternalError前者会原样拼接返回给模型后者只返回“服务暂时不可用”的模糊提示真正的问题留给开发者在后台看。这里有反直觉的一点给模型的错误信息不是越详细越好。如果你把一个 Python 堆栈直接发给模型模型会顺着堆栈“分析”出一堆不存在的结论反而误导后续回复。让模型感知到业务层面的错误就够了技术细节留在自己的日志里。4.4 技能输出太大上下文会被撑爆查询类技能很容易输出一大坨数据比如订单列表、商品详情、操作日志一次就能塞几百行 JSON。模型把这些内容全部读一遍既浪费 token又容易把注意力从用户问题上扯开。我的做法是给技能输出加一道压缩层。技能在返回结果前先按业务需要裁剪字段只保留用户关心的和模型决策需要的信息其余字段直接丢弃。比如用户查订单技能返回的不再是整条订单记录而是“状态已发货物流顺丰预计送达明天商品数量3件”。如果确实需要完整数据再通过一个专门的“获取订单原始数据”技能去取把它和面向用户的查询技能区分开。另外还有一个实用技巧当技能处理的是超大列表时输出中间态结果只给前几条和总数比如“共找到 256 条记录展示前 5 条”而不是把 256 条全部塞给模型。模型如果要更多会再发起一次带分页参数的调用。4.5 带副作用的技能必须走确认链路这个坑是我在一个模拟电商项目里翻过车之后才补上的。模型收到“帮我退款”的指令直接调了退款技能参数都对业务也执行成功了但用户其实想说的是“如果收货地址不对再退款”。打那以后我把技能分成了两类只读类查询、搜索、计算和写操作类修改、退款、发送。写操作类统一打上confirm_before_execute标记执行前必须向用户发起确认拿到明确同意之后才真正执行。这个确认不是简单让模型说一句“你确定吗”而是要把即将执行的动作、作用对象、影响范围都复述给用户比如“你确认要取消订单 202501150001 吗取消后优惠券会退回但需要 1 个工作日”。确认链路的加入让 Agent 看起来“慢”了半拍但换来的是安全性的大幅提升。对真实业务来说“少做一次操作”永远好过“做错一次操作”。5. 怎么保证技能的质量不是玄学5.1 给技能本身写单元测试技能本质上是函数是函数就应该有单元测试。我把每个技能 handler 的测试分成三组正常输入、边界输入、错误输入。正常输入验证返回内容的正确性边界输入测试空值、超长值、非法格式错误输入验证异常分支是否走了预期的错误返回。这些测试写得多了以后你会发现它其实是在给 Agent 系统的底层加油。模型调用技能时传参可能很随意但技能内部的逻辑必须可靠。如果技能层自己都是豆腐渣工程那模型再聪明也白搭Agent 系统的稳定性就是由这些最底层模块的稳定性决定的。5.2 测试模型的“会选技能”要单独做场景集技能逻辑测试是基础但同样重要的是测“模型会不会在正确的场景选对技能”。这个没法用传统单元测试覆盖我建了一个场景测试集每一条就是一段对话历史和期望的技能调用场景 001用户说“我的包裹怎么还没到” 期望调用 query_order或者关联的物流查询技能 禁止调用 update_address、apply_refund每条场景跑完统计正确调用率和误调率。这个测试集是活的每次在真实日志里发现模型误选技能就把对应的用户表达加进去然后调整描述再跑一遍。几轮下来误调率会很明显地下降。我建议场景测试的覆盖重点放在“边界场景”上用户表达模糊、用户同时提到两件事、用户情绪化表达、用户提供了缺参数的信息。这些场景才是模型最容易犯错的地方也是最能体现技能描述质量的试金石。5.3 用轨迹日志做复盘而不是只看结果评估 Agent 系统只盯着“用户满不满意”是完全不够的。我维护了一套全链路轨迹日志用户消息、召回的技能、模型选择的技能、传入的参数、技能执行结果、错误提示、模型的每一轮中间回复全部记录。复盘时我打开一条日志就像在看模型的思维草稿。有一次用户问“改到新地址大概多久能送到”日志显示模型先调了查询地址技能又调了物流时间估计技能最后拼出一段回答。这个思考过程是合理的。另一次模型在用户只是询问运费规则的时候调用了退款技能——幸好有日志不然这种低级错误根本发现不了。我个人的习惯是每周过一次误调日志把问题归类描述类问题、参数类问题、边界定义类问题、模型自身问题。分类之后前两类可以立刻改描述或改 Schema后一类可以考虑加规则约束或换更强的模型。有了这套复盘机制技能体系才会越用越准而不是越用越乱。6. 最后分享一点个人习惯做“agent-skills”这段时间我最大的体会是Agent 项目的复杂度从来不来自模型而来自围绕模型的那层工程系统。技能定义、注册、调度、校验、日志、测试每一环看起来都平平无奇但组合起来就是 Agent 能不能稳定工作的分水岭。如果你现在正准备从零搭一套技能体系我的建议是别急着炫技先把手头三个核心技能做成最小闭环描述写清楚、参数严格校验、输出格式化、异常可读化。把这三个练熟了再往多里扩展。技能数量一多你自然会体会到注册中心、预筛选、轨迹日志这些东西的价值到那时候再补也来得及。还有一个一直沿用的习惯每新增一个技能强制自己写满描述里的“触发场景”和“不要使用场景”两段。这两个字段看起来拖沓但对模型正确率的影响最大。宁可描述写得啰嗦一点也不要让模型在关键时刻猜错。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询