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

发布时间:2026/7/18 21:53:39

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. 返回值 Schemabr/明确返回的结构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[错误反馈给 LLMbr/要求重新填参] 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_length2, examples[Python asyncio 最佳实践], ) search_type: SearchType Field( defaultSearchType.CONTENT, description搜索类型title(标题匹配)、content(全文搜索)、tag(标签过滤), ) date_range: DateRange | None Field( defaultNone, description可选限定文档的发布日间范围, ) top_k: int Field( default5, ge1, le20, description返回的最相关文档数量默认 5最大 20, ) include_archived: bool Field( defaultFalse, 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( namesearch_documents, description( 在内部知识库中搜索文档。根据关键词或自然语言查询返回最相关的文档列表。 支持按标题、正文内容或标签进行搜索。可按日期范围过滤。 不支持修改或删除文档。搜索结果是只读的。 ), params_modelDocumentSearchParams, usage_examples[ 查找关于 Python asyncio 的文档 → search_documents(queryPython asyncio, search_typecontent, top_k5), ], error_scenarios[ {condition: 搜索关键词少于 2 个字符, response: {error: query_too_short}}, {condition: 无匹配结果, response: {results: [], total_hits: 0}}, ], ) print(json.dumps(tool_desc, ensure_asciiFalse, indent2)) # 模拟执行 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_asciiFalse, indent2)}) if __name__ __main__: logging.basicConfig(levellogging.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 消耗的平衡按场景动态裁剪工具列表降低冗余。

相关新闻