尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Agent技能库设计指南:从意图识别到工具调用的完整架构

Agent技能库设计指南:从意图识别到工具调用的完整架构 1. Agent-Skills 到底是什么我一直在琢磨一个问题大模型聊得好好的一到干正事就掉链子。让它总结一段文本还行让它去读一个文件、跑一段脚本、抓个网页再整理成表格它就开始胡言乱语。后来我把思路从提示词工程转到了技能编排才算是真正打开局面。所谓 agent-skills就是给大模型智能体设计一套可复用、可管理、可动态调用的技能库让模型不再靠临场发挥而是像工具箱里挑扳手一样按需调用预先定义好的能力。这个项目最核心的思路是把模型能做什么从模型会些什么里剥离出来。模型本身只是一个推理引擎它擅长的是理解意图、拆解步骤、生成内容但真正去执行文件操作、发起请求、计算数据、调用外部服务这些动作需要一套稳定、可控、可测试的代码模块来兜底。Agent 每次接到指令时系统会先把用户需求解析成任务描述再从技能库中匹配对应的技能项最后把技能执行结果回传给模型做进一步加工。整条链路里模型负责思考技能库负责执行。这个方案适合谁如果你正在搭建个人助理、自动化工作流、垂直领域问答系统或者你手上有一堆重复性的信息处理任务想交给大模型去干那这套思路会非常对路。它不挑具体框架不管是 LangChain、AutoGPT 还是自己手写的调度逻辑都可以用同样的方式来组织你的技能体系。我在实际落地过程中最大的体会是技能库设计得越干净Agent 的表现就越稳。很多时候模型表现不好不是模型笨而是我们给它的工具太乱了。2. 技能体系的整体架构设计2.1 技能的抽象层次别把所有功能堆在一个文件里技能体系的关键在于分层。我见过很多团队一上来就把十来个技能直接写成一个个函数然后塞给模型看起来挺灵活实际跑起来一塌糊涂。原因很简单技能描述太多模型在意图识别阶段就会迷路。正确做法是把技能分成三个层次。第一层是原子技能它是最小粒度的操作单元比如读取文件发送 HTTP 请求执行 SQL 查询计算文本向量生成图片。每一个原子技能只做一件事接口清晰输入输出可控方便单独测试。第二层是复合技能它负责把多个原子技能编排成一个完整流程。比如生成行业分析报告这个复合技能内部会调用搜索资料网页摘要数据提取文本生成四个原子技能最终产出一份结构化报告。第三层是元技能它不直接执行操作而是负责感知当前任务状态、决定调用哪个复合技能、判断上一步结果是否符合预期。这个分层的好处非常明显模型不需要关心如何读文件这种底层细节它只需要知道有这么一个技能叫读取文件。需要组合时复合技能内部再去做调度和协调。如果你把底层的文件操作、网络请求逻辑全部暴露给模型模型反而会因为信息过载而判断失误。这就像你给实习生安排工作你不会告诉他这个 Excel 函数怎么写、那个 API 怎么调你只需要说清楚要什么结果就行。2.2 技能注册给每个技能一份简历技能不是写好了就完事你得让它能被 Agent 发现、被模型理解。我在这里用了一个注册制的设计每个技能在系统启动时都会向技能注册中心提交一份个人档案包含技能名称、功能描述、参数定义、返回格式、使用限制和使用示例。技能名称要短小精确比如 search_web get_page_content generate_image不要搞成 do_the_web_search_thing 这种啰嗦写法。功能描述是给大模型看的它承担着意图匹配的重任所以一定要写清楚这个技能什么时候用、什么时候不要用、有什么前置条件。参数定义必须使用 JSON Schema 格式明确每个参数的类型、必填性、取值范围和默认值。返回格式统一用标准结构包装至少要包含 status、data、error 三个字段这样后续调度逻辑才好统一处理。注册完的技能会集中在一个索引文件里Agent 每次发起任务时系统会结合用户的自然语言输入和系统 prompt 中的技能说明让模型自己选择合适的技能并生成参数。如果你的模型不支持复杂的函数调用你也可以直接用规则匹配或 embedding 相似度来做技能路由这块我在后面实操部分会详细演示。总的来说注册环节省下的心思会在后面排查问题的阶段加倍还给你千万别偷懒。2.3 技能注入策略上下文窗口是稀缺资源还有一个很容易被忽视的问题——你不可能把所有技能的完整描述每轮都塞给大模型。假设你有四十个技能每个技能描述算二百个 token加起来就是八千个 token这还不算参数 schema。模型上下文窗口虽然越来越大但你留给推理的空间就会越来越小响应速度也会明显下降。我的做法是做动态注入不是全量注入。系统先做一个轻量的预筛——根据用户问题里的关键词和语义从技能索引里选出最可能相关的五到八个技能然后把它们的完整描述放入当前轮的上下文。剩下的技能只保留一行摘要实在用不到就完全不提。这样既保证了模型有足够信息做决策又不至于把上下文撑爆。另外要注意技能描述的书写顺序。模型在处理长文本时对开头和结尾的内容注意力更强。所以技能描述的第一句话必须是一句高度概括的功能声明比如搜索公开网页并返回前 N 条结果摘要剩下的细节放到后面。我对比过不同描述顺序对意图识别准确率的影响差异在三到五个百分点左右虽然不是翻天覆地但考虑到这是一个免费优化项性价比很高。3. 核心技能的定义与调度3.1 技能描述 Schema 的设计细节技能描述 Schema 是整个体系的根这一步做好了后面所有环节都会省力。我是从函数调用Function Calling的规范里受到启发经过几轮迭代后固定成下面这套结构。{ name: web_search, description: 在公网上搜索给定关键词返回前N条结果的标题、链接和摘要。适用于查资料、找文章、确认信息。不适用于访问需要登录的站点。, parameters: { type: object, properties: { query: { type: string, description: 搜索关键词尽量精简建议不超过10个字, maxLength: 30 }, top_k: { type: integer, description: 返回结果数量默认5最大10, minimum: 1, maximum: 10, default: 5 } }, required: [query] }, returns: { type: array, items: { type: object, properties: { title: {type: string}, url: {type: string}, snippet: {type: string} } } }, examples: [ 把大模型 Agent 实践相关的最新文章找出来, 搜索Python 异步编程 最佳实践只要前三条, 查一下2025年大模型行业的发展趋势 ] }在 description 里加什么时候不要用这个信息是我踩过坑之后补上的。没有这条之前用户问帮我打开某某网站模型可能会误选 search 技能而不是 page_open 技能导致拿到的只是搜索结果而不是网页正文。加了边界描述之后这种误判明显减少。examples 字段也很管用模型在少样本场景下对具体例子的理解能力远比对抽象规则的理解能力强。3.2 技能路由意图识别与参数生成技能路由有两个主流方案。一个是大模型直接输出结构化函数调用这需要模型支持 function call 能力市面上主流模型基本都支持。另一个是嵌入向量匹配把所有技能描述向量化然后用用户输入去检索最相似的。前者的优点是能同时生成参数缺点是模型可能自创技能名后者的优点是稳定可控缺点是你得另外想办法提取参数。我现在的实现里混合了两种思路。第一步先用向量匹配挑出 top 5 候选技能第二步把候选技能的完整描述发送给大模型让它从中选择一个并给出参数。这样做的原因很实际模型在处理候选列表时相当于做选择题比做填空题靠谱得多而且如果模型给出的技能名不在候选列表里我可以立刻拦截这次调用并触发重试不会出现模型异想天开调用了一个不存在技能的情况。参数生成这块有个容易忽略的坑模型倾向于按照用户原话里的信息填参数而不是按照 schema 处理。比如用户说帮我搜一下大模型相关的文章要最近一年的模型可能会把最近一年当作 query 的一部分塞进去实际上 search 接口根本不支持时间过滤。解决办法是在 schema 里把时间字段单独做成一个可选参数并且再给 description 加一句如果用户提到时间范围提取到 time_range 字段不要放入 query。这类约束写多了之后参数的准确率能到九成以上。3.3 技能执行的上下文管理技能不是孤立执行的它需要访问一些共享信息比如用户身份、当前工作目录、会话历史、临时文件路径等。我一开始把需要的数据一股脑塞进每个技能函数的形参里后来发现维护成本实在太高每加一个技能就要改一遍参数列表。后来我改成了上下文对象模式把所有共享信息挂在一个 Context 对象上执行技能时整个传进去。上下文对象至少要包含这几类信息会话级状态用户 ID、对话轮次、语言偏好、任务级状态当前任务的目标、中间产物、已调用的技能列表、环境信息临时目录、API Key、模型配置。这样设计的好处是技能与技能之间通过上下文做数据交换不需要在函数签名里写一堆耦合参数。比如一个技能生成了中间结果临时文件它把文件路径写入上下文的 task_state 里下一个技能直接从 state 里读取链路非常清晰。还有一点要特别注意技能执行过程中可能产生敏感信息比如 API Key、用户隐私数据。在把结果回传给大模型之前我会统一做一遍脱敏处理把 token、密码打码再进入后续流程。这一步不仅是为了安全合规也是为了防止大模型在后续回复中无意间泄露这些信息。4. 完整实操从零搭一个 Agent 技能库4.1 环境准备与项目结构实操部分我用 Python 来演示因为生态最完善、示例最好复现。你需要准备一个 Python 3.10 环境安装 openai或者其他模型 SDK、numpy做向量运算和 jieba做中文分词后面路由匹配时要用。项目结构我建议这样组织agent-skills/ ├── core/ │ ├── __init__.py │ ├── context.py # 上下文对象 │ ├── registry.py # 技能注册中心 │ └── dispatcher.py # 技能调度器 ├── skills/ │ ├── __init__.py │ ├── text_tools.py # 文本处理类技能 │ ├── web_tools.py # 网络请求类技能 │ └── analysis_tools.py# 数据分析类技能 ├── tests/ │ └── test_skills.py # 技能单元测试 └── main.py # 主入口演示整个流程这个结构是我几轮迭代后的最终版。核心逻辑写在 core 里具体技能分布在 skills 目录tests 目录负责保障每个技能在改动之后还能正常工作。main.py 用来做端到端演示方便你跑通整个链路感受一下效果。前期不要追求功能多先把最小闭环跑起来再加技能。4.2 技能注册中心与上下文实现先写核心的注册中心和上下文类。我用一个装饰器来注册技能这样新增技能时只需在函数定义处加一行标记不用动注册中心代码扩展性很好。# core/registry.py from typing import Dict, Callable, Any, Optional class SkillRegistry: def __init__(self): self._skills: Dict[str, Dict[str, Any]] {} def register(self, name: str, description: str, parameters: dict, returns: dict, examples: list[str] None): def decorator(func: Callable): self._skills[name] { name: name, description: description, parameters: parameters, returns: returns, examples: examples or [], func: func, } return func return decorator def get(self, name: str) - Optional[Dict[str, Any]]: return self._skills.get(name) def list_skills(self) - list[str]: return list(self._skills.keys()) def match_by_text(self, query: str, top_k: int 5) - list[str]: # 先用简单的关键词匹配做候选筛选 # 实际项目中可以换用 embedding 检索 scores {} for name, meta in self._skills.items(): desc meta[description] score 0 for token in set(jieba.lcut(query)): if token in desc or token in name: score 1 # 在描述和name中出现次数越多得分越高 scores[name] score sorted_scores sorted(scores.items(), keylambda x: x[1], reverseTrue) return [name for name, score in sorted_scores[:top_k] if score 0] registry SkillRegistry()# core/context.py from dataclasses import dataclass, field from typing import Dict, Any dataclass class TaskContext: user_id: str language: str zh session_state: Dict[str, Any] field(default_factorydict) task_state: Dict[str, Any] field(default_factorydict) temp_dir: str ./tmp env: Dict[str, Any] field(default_factorydict) def set_state(self, key: str, value: Any): self.task_state[key] value def get_state(self, key: str, default: Any None): return self.task_state.get(key, default)注意 jieba 的引入在 registry.py 里没有显示实际文件头要写import jieba。关键词匹配只是一个兜底方案如果你的模型接口本来就支持 function calling可以直接让模型从技能列表里做选择这个关键词匹配主要用在模型能力不够或者不想消耗额外 token 的场景。4.3 两个标准技能的完整实现下面我写两个有代表性的技能一个是文本处理类的文本摘要一个是网络操作类的抓取网页正文并提取标题。前一个展示的是纯本地处理型技能后一个展示的是带网络请求的外部交互型技能两个完全可以覆盖大多数场景。# skills/text_tools.py import hashlib from core.registry import registry from core.context import TaskContext registry.register( nametext_summarize, description对输入的长文本生成简洁摘要。适用于文章、报告、会议纪要等文本的压缩总结。注意输入文本不能超过2万字超过时请先分段。, parameters{ type: object, properties: { text: {type: string, description: 需要摘要的原始文本}, max_length: {type: integer, description: 摘要最大字数, default: 200} }, required: [text] }, returns{type: string, description: 生成后的摘要文本}, examples[把下面这段会议纪要提炼成100字以内的要点] ) def text_summarize(context: TaskContext, text: str, max_length: int 200): # 这里接入大模型的生成接口 # 演示时先用一个最简单的截断逻辑代替 processed text.strip().replace(\n, ) if len(processed) max_length: return processed # 真正的实现应该调用 LLM这里为了演示直接取了开头部分 return processed[:max_length] ……# skills/web_tools.py import requests from bs4 import BeautifulSoup from core.registry import registry from core.context import TaskContext registry.register( namefetch_webpage, description抓取指定URL的网页正文内容返回标题和正文纯文本。适用于查看文章、获取网页信息。不适用于需要登录或验证码的页面。, parameters{ type: object, properties: { url: {type: string, description: 完整的网页地址必须包含http或https前缀} }, required: [url] }, returns{type: object, properties: {title: {type: string}, content: {type: string}}}, examples[把这篇文章的内容抓下来https://example.com/post/123] ) def fetch_webpage(context: TaskContext, url: str): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) title soup.title.string.strip() if soup.title else # 去掉script和style标签保留正文文本 for tag in soup([script, style, nav, footer]): tag.decompose() content soup.get_text(separator\n, stripTrue) return {title: title, content: content[:5000]}在实际项目中文本摘要不要用截断逻辑糊弄一定要接大模型来生成。但即使接大模型也要在技能函数里做最基本的输入清洗、长度检测给异常情况做好兜底。网络请求类的技能更要注意超时设置和异常捕获用户在真实环境里网络状况千奇百怪技能函数如果动不动就抛异常整个 Agent 流程就会断掉。4.4 调度器与主流程串联注册中心和技能函数都有了接下来写调度器把用户输入-技能匹配-参数提取-技能执行-结果返回这条链路串起来。这里我保留了一个 hook 接口方便你接入真正的大模型调用。# core/dispatcher.py import json from typing import Optional from core.registry import registry from core.context import TaskContext class SkillDispatcher: def __init__(self, llm_routerNone): self.llm_router llm_router def dispatch(self, user_input: str, context: TaskContext) - dict: # 步骤1用文本匹配选出候选技能 candidates registry.match_by_text(user_input, top_k5) if not candidates: return {status: error, message: 没有找到匹配的技能} # 步骤2把候选技能信息给大模型路由让它决定用哪个技能并生成参数 if self.llm_router is not None: skill_name, params self.llm_router(candidates, user_input) else: # 兜底方案直接用第一个候选技能 空参数 skill_name, params candidates[0], {} skill_meta registry.get(skill_name) if not skill_meta: return {status: error, message: f技能 {skill_name} 不存在} # 步骤3执行技能 try: result skill_meta[func](context, **params) return {status: ok, skill: skill_name, result: result} except Exception as e: return {status: error, skill: skill_name, message: str(e)}# main.py from core.context import TaskContext from core.dispatcher import SkillDispatcher from skills import text_tools, web_tools # noqa: F401 确保技能被注册 def mock_llm_router(candidates, user_input): 演示用的路由函数实际项目中替换为真实LLM调用 # 简单规则如果用户输入包含抓取/打开/网页/链接就用web技能 for candidate in candidates: if fetch_webpage in candidate and any(k in user_input for k in [抓取, 打开, 网页, 链接, http]): return fetch_webpage, {url: https://example.com} return candidates[0], {} if __name__ __main__: ctx TaskContext(user_idtest_user) dispatcher SkillDispatcher(llm_routermock_llm_router) test_input 帮我抓取一下这个网页的内容https://example.com result dispatcher.dispatch(test_input, ctx) print(json.dumps(result, ensure_asciiFalse, indent2))mock_llm_router 这段代码演示的是规则路由能跑通流程但很粗糙。真实项目里你应该把 candidates 和 user_input 一起发给大模型让它输出 JSON 格式的技能选择结果。我在之前的一个项目里让大模型输出的格式是{skill: fetch_webpage, params: {url: https://...}}然后用json.loads解析。如果解析失败或 key 缺失就触发一次重试把错误信息回传给模型让它修正。这个重试机制能大幅提高参数生成的容错率。4.5 技能测试与效果评估技能库做到一定规模后回归测试就成了刚需。每加一个新技能我都建议顺手写几个测试用例。测试不用复杂重点覆盖三类场景正常调用、参数缺失、异常输入。参数缺失指的是模型漏传了必填参数这时候你的调度逻辑应该返回清晰的错误提示而不是直接抛异常。异常输入指的是用户让技能去做超出边界的事情比如 fetch_webpage 收到一个非 http 协议的内容或者 text_summarize 收到一个空字符串。# tests/test_skills.py import pytest from core.context import TaskContext from core.dispatcher import SkillDispatcher from skills import text_tools, web_tools # noqa def test_text_summarize_basic(): ctx TaskContext(user_idtester) result text_tools.text_summarize(ctx, 这是一段测试文本内容长度远超摘要长度。) assert isinstance(result, str) assert len(result) 250 def test_fetch_webpage_bad_url(): ctx TaskContext(user_idtester) with pytest.raises(Exception): web_tools.fetch_webpage(ctx, not_a_url)跑一遍pytest确保原有技能没被改坏。我自己的习惯是每次改完技能代码固定跑一遍全量测试如果某个技能的返回结构变了立刻就能发现。别看这个习惯简单它能帮你省掉大量联调阶段排查诡异问题的时间。技能库里的技能本质上和普通软件里的函数一样一样要有版本管理、测试覆盖和变更记录。5. 常见问题与排查技巧实录5.1 意图识别不准技能老是选错这是 Agent 技能体系里最常碰到的问题。你让模型抓取网页它偏要去调搜索你让它做文本摘要它给你调用了一个翻译技能。排查思路通常分三步。先看技能描述是不是太模糊有没有写清楚这个技能什么时候不该用。我见过太多技能描述只写了功能没写边界模型只能靠猜。再看候选技能列表是不是干扰太大一次给五六个相似度都很高的技能模型自然容易迷糊。最后看你的技能名和描述里用词是否和用户习惯一致。我做过一个测试一个技能叫 extract_json描述里全是提取 JSON 字段结果用户说帮我把这段数据里的名字和邮箱抠出来模型匹配了老半天才选中它。后来我把描述改成从文本或JSON字符串中提取指定字段信息比如姓名、邮箱、手机号、地址等准确率立刻上来了。技能描述应该用用户视角的语言去写而不是用开发者视角的术语去写。5.2 参数生成乱套漏传或误传参数问题比选错技能更隐蔽因为它不会直接报错而是会悄悄影响输出质量。最常见的两个问题是必填参数漏传字符串参数里塞进了无关文本。漏传的锅在于 schema 描述不够清楚模型不知道这个参数该填什么误传的锅在于模型理解不了参数边界。我的经验是给每个参数都写一个用户可能怎么说的锚点示例放在 description 最前面。比如 query 参数的描述写上搜索关键词例如大模型、Agent、技能编排。模型看到示例之后即使面对表达方式很多变的用户输入也能准确提取。另外在调度器里做好参数校验必填参数缺失时可以主动回传给模型提示你的参数少了xx字段请补全后重新生成这比后端直接报错要友好得多。5.3 技能越来越多上下文快被塞爆技能库从最开始的五个技能增长到三十个之后全量注入的问题就开始爆发。除了前面提到的动态候选筛选还有一个优化方向是给技能做分组。比如文本处理组网络操作组数据分析组每个组有一个统一描述模型先选组再选具体技能两跳的方式能把每轮注入的 token 控制在很小范围。另外技能描述本身也要做减法。我之前喜欢把怎么实现、用什么库写进描述里后来发现模型根本不关心这些。功能说清楚、边界说清楚、参数说清楚就足够了。那些实现细节留给开发者在代码里看不要占用宝贵的上下文窗口。5.4 常规文档里永远不会写的经验总结最后给你几个我在真实项目里砸了不少时间才总结出来的注意点都是常规文档不会告诉你的事情。第一项技能执行结果一定要做压缩。网页抓取技能返回五千字正文如果你直接把这个结果原封不动塞给大模型去生成回复一方面 token 消耗大另一方面模型的注意力会被无关内容干扰。正确做法是先做一次摘要或关键信息提取把结果压缩到三百字以内再进入模型上下文。这个压缩步骤可以单独做成一个后处理技能效果立竿见影。第二项给技能加统一的超时控制。我现在所有技能内部都设了合理的超时上限网络请求类十秒本地计算类三十秒。技能超时之后返回一个超时标记调度器可以把这个标记回传给模型让它换个策略或者告知用户稍后再试。如果没有超时控制一个卡住的外部请求能让整个 Agent 流程停滞好几分钟。第三项技能之间尽量不要共享可变状态。虽然前面设计了 task_state 让技能之间能交换中间产物但共享状态越少越好。我在一个项目里让两个技能共用一个临时列表结果一个技能清空了列表另一个技能拿到空数据查了半下午才发现是状态互相干扰。后来我强制规定技能只能通过返回值传递数据只有跨多个技能且确实需要共享的中间文件路径才写入 task_state。第四项每个技能都要写日志。不用复杂函数入口和出口各打一条日志包含技能名称、关键参数、耗时、结果状态。很多诡异的问题比如模型选错技能、参数生成离谱、技能执行了但结果不符合预期有了日志之后都能快速定位。没有日志的情况下排查问题就像闭眼摸黑有了日志就相当于开了灯。6. 后续扩展方向与个人体会现在这套技能体系已经能支撑不少日常任务了但我还在持续打磨一些方向。最值得做的是把技能库变成可插拔的生态技能的描述和实现做成插件形式统一发布到一个目录里其他项目可以直接复用。这就像把工具箱里的每个工具都贴上标签、做好保养任何人拿去都能直接用。另一个方向是让技能自己学会反馈改进。技能执行失败或者结果质量差时系统把反馈信息记录下来定时做统计分析找出出错率最高的技能提醒开发者去优化。这个机制我还在试验阶段目前只是把每个技能的失败日志单独收集起来每周看一次聚合报告。从我个人的实际使用体验来说agent-skills 这套方法论彻底改变了我对 Agent 开发的认知。以前我是把宝全押在模型的推理能力上希望模型自己悟出怎么完成任务现在的做法是把推理和执行拆开让模型专心做推理让技能稳稳地做执行。这种拆分带来的稳定性提升是肉眼可见的尤其在任务链路长、步骤多、对结果准确性要求高的场景下效果极其明显。最后分享一个小技巧当你觉得 Agent 表现不稳定时先别急着换模型或调 prompt优先检查你的技能库。技能描述是否清晰、返回结构是否统一、异常处理是否完备、日志是否完整——这些问题查过一轮之后大部分模型抽风现象其实都能找到具体原因。把基础打扎实Agent 的表现自然就稳了。
返回列表