当前位置: 首页 > news >正文

LangChain 工具描述语言:如何写出让模型准确理解工具用途的描述

LangChain 工具描述语言:如何写出让模型准确理解工具用途的描述

一、深度引言与场景痛点

Agent 开发里有一个让人哭笑不得的现象:你给模型配了 5 个工具,结果它只用其中 2 个,另外 3 个从未被调用——不是因为不需要,而是模型压根没理解那几个工具是干什么的。更糟糕的是误调用:模型把"查天气"的工具用来"查股票",因为两个工具的描述里都有"查询"二字。

工具描述(Tool Description)是模型理解工具的唯一途径。模型看不到工具的实现代码,不知道工具的内部逻辑,它只能根据你给的文字描述来决定"要不要用、用什么参数"。如果描述模糊、缺少参数约束、没有示例——模型就会"猜",猜错的概率和描述的模糊程度成正比。

这个问题在 OpenAI 的 function calling 和 LangChain 的 tool 定义中同样存在。底层机制都是把工具描述转换成 JSON Schema 后注入 System Prompt,模型基于 Schema 决定输出一个 function_call。Schema 的质量直接决定了模型调用的准确率。

二、底层机制与原理深度剖析

一个好的工具描述需要覆盖五个维度,缺一不可:

flowchart LR subgraph tool_def["工具定义五要素"] D1["1. 功能声明<br/>用一句话说清工具做什么<br/>正例: '查询指定城市的实时天气'<br/>反例: '天气工具'"] D2["2. 参数约束<br/>每个参数的类型、范围、是否必填<br/>使用 enum 限制可取值<br/>用 description 说明含义"] D3["3. 返回值 Schema<br/>明确返回的结构<br/>让模型知道能拿到什么信息"] D4["4. 使用示例<br/>1-2 个典型调用场景<br/>输入→输出对照"] D5["5. 异常场景说明<br/>什么情况下会失败<br/>失败时返回什么<br/>模型应该怎么处理"] end subgraph pipeline["描述→调用链路"] A["工具描述<br/>(OpenAI Function Schema)"] --> B["LLM 解析<br/>匹配意图→选择工具→填参数"] B --> C["参数校验<br/>(Pydantic 类型验证)"] C -->|"通过"| D["执行工具调用"] C -->|"失败"| E["错误反馈给 LLM<br/>要求重新填参"] end D1 --> A D2 --> A D3 --> A D4 --> A D5 --> A style D1 fill:#e3f2fd,stroke:#1565c0 style D2 fill:#e3f2fd,stroke:#1565c0 style D3 fill:#e3f2fd,stroke:#1565c0 style D4 fill:#fff3e0,stroke:#ef6c00 style D5 fill:#f3e5f5,stroke:#7b1fa2

功能声明是最重要也最容易写坯的部分。差的声明只说了工具的名字("数据库查询工具"),好的声明说清了工具的职责边界("在用户订单数据库中根据订单号、手机号或时间范围查询订单详情,不支持修改和删除操作")。边界表述的"不支持 XXX"能有效减少模型的误调用。

参数约束需要特别关注 enum 类型。如果某个参数只能取特定几个值(如搜索类型只能是"title"或"content"),一定要用 enum 声明而不是写在 description 里。模型对 enum 的理解准确率远高于对自然语言 description 中约束的理解。

返回值 Schema 告诉模型"调用这个工具后你能得到什么信息",帮助模型判断是否需要进一步处理。比如返回了{"order_status": "shipped", "tracking_number": "SF123456"},模型就知道可以直接回答"您的订单已发货,快递单号是 SF123456",而不需要再调其他工具。

使用示例放在 description 中作为"软约束"。模型会从示例中学习工具调用的典型模式,这和 Few-shot 的原理一样。示例应该真实反映工具的典型用法,而不是为了凑数随便写。

异常场景说明是可选的,但它能显著提升工具的鲁棒性。比如"如果订单号不存在,工具会返回{"error": "order_not_found"},请提示用户核对订单号"——这让模型知道失败是正常情况,不需要反复重试。

三、生产级代码实现

import json import logging from datetime import datetime from enum import Enum from typing import Any, Literal from pydantic import BaseModel, Field, ValidationError logger = logging.getLogger(__name__) # ── 参数定义(Pydantic 模型即 Schema)── class SearchType(str, Enum): TITLE = "title" CONTENT = "content" TAG = "tag" class DateRange(BaseModel): start: str = Field(description="开始日期,格式 YYYY-MM-DD", examples=["2026-01-01"]) end: str = Field(description="结束日期,格式 YYYY-MM-DD,必须 >= start", examples=["2026-06-30"]) class DocumentSearchParams(BaseModel): """搜索知识库文档的参数""" query: str = Field( description="搜索关键词或自然语言查询。支持中英文。至少 2 个字符。", min_length=2, examples=["Python asyncio 最佳实践"], ) search_type: SearchType = Field( default=SearchType.CONTENT, description="搜索类型:title(标题匹配)、content(全文搜索)、tag(标签过滤)", ) date_range: DateRange | None = Field( default=None, description="可选,限定文档的发布日间范围", ) top_k: int = Field( default=5, ge=1, le=20, description="返回的最相关文档数量,默认 5,最大 20", ) include_archived: bool = Field( default=False, description="是否包含已归档文档,默认不包含", ) # ── 返回值定义 ── class SearchResult(BaseModel): doc_id: str = Field(description="文档唯一 ID") title: str = Field(description="文档标题") snippet: str = Field(description="匹配内容的摘要片段") score: float = Field(description="相关性分数,0-1 之间,越高越相关") updated_at: str = Field(description="文档最后更新时间") class DocumentSearchResponse(BaseModel): results: list[SearchResult] = Field(description="搜索结果列表,按相关性降序排列") total_hits: int = Field(description="符合条件的文档总数(可能大于返回数量)") search_time_ms: float = Field(description="检索耗时(毫秒)") # ── 工具描述生成器 ── class ToolDescriptionGenerator: """从 Pydantic 模型自动生成 OpenAI Function Calling 格式的工具描述""" @staticmethod def generate( name: str, description: str, params_model: type[BaseModel], usage_examples: list[str] | None = None, error_scenarios: list[dict[str, str]] | None = None, ) -> dict[str, Any]: """生成完整的 function calling schema""" schema = params_model.model_json_schema() # 构建增强的 description enhanced_desc = description if usage_examples: enhanced_desc += "\n\n使用示例:\n" + "\n".join( f"- {ex}" for ex in usage_examples ) if error_scenarios: enhanced_desc += "\n\n异常处理:\n" + "\n".join( f"- {err['condition']}:返回 {err['response']}" for err in error_scenarios ) # OpenAI function calling 格式 return { "type": "function", "function": { "name": name, "description": enhanced_desc, "parameters": { "type": "object", "properties": schema.get("properties", {}), "required": schema.get("required", []), }, }, } @staticmethod def generate_full_toolset( tools: list[dict[str, Any]], system_prompt_additions: str = "", ) -> tuple[list[dict[str, Any]], str]: """生成完整的工具集和配套 System Prompt""" tool_names = [t["function"]["name"] for t in tools] system_prompt = ( "你可以使用以下工具来完成任务。调用工具前请仔细阅读工具描述," "确保参数类型正确、必填参数完整。\n" f"可用工具:{', '.join(tool_names)}\n" f"{system_prompt_additions}" ) return tools, system_prompt # ── 带校验的工具执行器 ── class ToolExecutor: """工具执行器:参数校验 + 执行 + 错误反馈""" def __init__(self, tools_registry: dict[str, Any]) -> None: self._registry = tools_registry async def execute( self, tool_name: str, arguments: dict[str, Any] ) -> dict[str, Any]: """执行工具调用,含参数校验和错误处理""" if tool_name not in self._registry: return { "error": f"未知工具 '{tool_name}',可用工具:{list(self._registry.keys())}", "suggestion": "请使用可用工具列表中的工具", } tool_info = self._registry[tool_name] # Pydantic 参数校验 try: params = tool_info["params_model"](**arguments) except ValidationError as e: return { "error": "参数校验失败", "details": json.loads(e.json()), "suggestion": f"请检查参数类型和必填字段,参考 Schema:{tool_info['schema']}", } # 执行工具 try: result = await tool_info["handler"](params) return result except Exception as e: logger.exception("Tool execution failed: %s", tool_name) return { "error": f"工具执行失败: {str(e)}", "retryable": tool_info.get("retryable", False), } async def main() -> None: # 注册工具 tools_registry = { "search_documents": { "params_model": DocumentSearchParams, "handler": lambda p: { "results": [ {"doc_id": "1", "title": "AsyncIO 指南", "snippet": "...", "score": 0.95, "updated_at": "2026-06-01"} ], "total_hits": 1, "search_time_ms": 12.5, }, "retryable": True, }, } # 生成工具描述 generator = ToolDescriptionGenerator() tool_desc = generator.generate( name="search_documents", description=( "在内部知识库中搜索文档。根据关键词或自然语言查询返回最相关的文档列表。" "支持按标题、正文内容或标签进行搜索。可按日期范围过滤。" "不支持修改或删除文档。搜索结果是只读的。" ), params_model=DocumentSearchParams, usage_examples=[ "查找关于 'Python asyncio' 的文档 → search_documents(query='Python asyncio', search_type='content', top_k=5)", ], error_scenarios=[ {"condition": "搜索关键词少于 2 个字符", "response": '{"error": "query_too_short"}'}, {"condition": "无匹配结果", "response": '{"results": [], "total_hits": 0}'}, ], ) print(json.dumps(tool_desc, ensure_ascii=False, indent=2)) # 模拟执行 executor = ToolExecutor(tools_registry) result = await executor.execute("search_documents", { "query": "Python asyncio", "search_type": "content", "top_k": 3, }) print(f"\n执行结果: {json.dumps(result, ensure_ascii=False, indent=2)}") if __name__ == "__main__": logging.basicConfig(level=logging.INFO) import asyncio asyncio.run(main())

代码的核心设计:Pydantic 的model_json_schema()自动将 Python 类型注解转换成 JSON Schema,包括 min_length、ge/le 约束和 enum 限制——这些约束被 OpenAI function calling 原生支持,模型在生成参数时就会遵守。ToolDescriptionGenerator在基础 description 之上追加使用示例和异常场景,形成完整的工具说明书。ToolExecutor在调用前强制做 Pydantic 校验,即使模型生成的参数不合法也能捕获并反馈——形成了一个"校验→反馈→重试"的闭环。

四、边界分析与架构权衡

工具描述的详细度和 Token 消耗是一对矛盾。每多一点描述就多一些 Token 消耗(System Prompt 每次请求都要带上所有工具描述)。如果 10 个工具每个 200 字的描述,总计 2000 字 ≈ 500 tokens——看起来不多,但每天百万次请求就是 5 亿 Token,成本可观。

优化方向:一是做"分级描述"。在 API 请求里只放核心的功能声明和参数 Schema(精简版),完整的使用示例和异常场景放在文档里或检索增强的 context 中。二是按场景动态裁剪工具列表——不是每次请求都给模型全部 50 个工具,而是根据用户意图先筛出最可能的 5-10 个工具,大幅减少 Prompt 中的工具描述总量。

工具描述和 Prompt 里的其他信息之间存在"注意力竞争"。如果 Prompt 里有一千字的长篇系统指令,模型对工具描述的注意力会被稀释。建议把工具描述放在 System Prompt 的最后面(模型对 Prompt 尾部的注意力最集中),并且用### 可用工具 ###这样的 Markdown 标题分隔,增强模型的关注。

五、总结

工具描述是 Agent 和模型之间的唯一接口文档。好的描述覆盖五个维度:功能声明(说清工具的职责边界)、参数约束(用类型系统和 enum 限制输入空间)、返回值 Schema(让模型知道拿到什么)、使用示例(教模型怎么用)、异常场景说明(告诉模型失败了怎么办)。用 Pydantic 自动生成 Schema 避免手写错误,用参数校验 + 错误反馈形成闭环。控制描述的详细度与 Token 消耗的平衡,按场景动态裁剪工具列表降低冗余。

http://www.jsqmd.com/news/1215714/

相关文章:

  • 如何从0到1构建自动化的咖啡和冰激凌机器人?
  • 2026年7月最新泉州宝玑官方售后客户服务电话及线下网点地址 - 亨得利钟表维修中心
  • Gemini图片识别准确率提升73%的5个隐藏参数配置:工程师绝不会告诉你的调试秘籍
  • 2026年7月水墨印刷开槽机实力厂家推荐,电脑控制高速水墨印刷开槽机/印刷联动线,水墨印刷开槽机加工厂口碑推荐 - 品牌推荐师
  • 从0到1精通Avocado-VT:虚拟化测试工程师的完整成长路线图
  • 2026年7月最新亨得利官方服务项目及价格查询|网点地址和热线权威信息公示 - 亨得利官方
  • Node.js 独立产品高可用架构:从单点到集群的容错设计
  • 3分钟扫码获取阿里云盘Refresh Token:安全授权终极指南
  • 生成式AI赋能大学英语告诫语言能力培养平台-开发日志(Day 2)
  • 9款AI论文写作软件:一键生成开题报告、论文大纲、学位论文与期刊论文
  • 甲骨文云充值入口在哪?账号付款设置说明
  • 2026年永康GEO供应商榜单前十深度解析 - GrowUME
  • 双非新生用热爱敲开计算机世界
  • 2026年有没有一键给图片加水印的工具?亲测好用的免费方法 - 图片处理研究员
  • python 1 大疆照片rtk gps 信息提取 定位精度 - MKT
  • Python 内存优化实战:用 tracemalloc 和 objgraph 定位 RAG 服务内存泄漏
  • 语音识别准确率对比:从WER到SER再到语义F1,为什么99%的评测报告都在误导你?(附开源评估框架)
  • 劳力士中国专柜官方电话2026年7月最新客户服务通告 - 劳力士官方服务中心
  • 2026年最新指南:大学生必考哪几个证书好?高含金量与避坑全解析
  • 门槛低含金量高的资格证书有哪些?2026年普通人逆袭、零基础转行必看指南
  • 网站还在用HTTP?不安全协议怎么升级到HTTPS,全流程迁移指南
  • GEO内容发了一堆,AI却视而不见,究竟是哪里出了偏差?
  • poissonsearch-py批量操作终极指南:高效处理大规模Elasticsearch数据导入导出
  • Gemini音频转录精度提升72%:实测5种噪声场景下的参数调优全流程(附可复用Python脚本)
  • 一名数据科学专业学生的编程学习规划与思考
  • 金小福黄金回收全维度业务范围详解 - GrowUME
  • 2026年7月最新济南爱彼官方售后热线及客户服务网点地址 - 爱彼中国官方服务中心
  • 亲身到店体验北京亨得利官方名表服务中心|服务电话及全部维修地址(2026年7月更新) - 亨得利官方博客
  • YOLO训练中Random Seed固定为什么不够?影响训练可复现性的6个隐藏因素
  • Canva AI品牌套件落地全路径(从零搭建→团队协同→API集成→合规审计)