Day 27:工具设计原则 —— 写好 Docstring 让模型更聪明
欢迎来到第二十七天!在构建 Agent 的过程中,工具的质量直接决定了 Agent 的性能。模型能否在正确的时机调用正确的工具,并传递正确的参数,很大程度上取决于我们如何描述工具。今天我们将深入探讨工具设计的核心原则,重点理解Docstring(文档字符串)对模型行为的影响,并通过对比实验,让你亲眼看到模糊描述与清晰描述带来的巨大差异。
一、今日学习目标
- 理解工具描述(Docstring)在 Agent 中的关键作用:它是模型了解工具的唯一渠道。
- 掌握编写高质量工具描述的原则:明确功能、输入输出格式、使用场景、限制条件。
- 学会为参数提供清晰的说明,使用
Annotated描述参数含义、类型和示例。 - 通过实验:定义一个模拟查询公司内部员工薪资的假接口,分别用模糊描述和详细描述,观察模型何时决定调用它、参数是否正确。
- 能够根据实际需求设计出易于被模型理解并正确调用的工具。
二、详细实现步骤
步骤 1:准备基础环境
与前几天类似,我们使用 LangChain 和 DeepSeek。确保已安装所需库,并初始化模型。
新建tool_design_principles.py:
importosfromdotenvimportload_dotenvfromlangchain_openaiimportChatOpenAIfromlangchain_core.toolsimporttoolfromtypingimportAnnotated load_dotenv()llm=ChatOpenAI(model="deepseek-chat",api_key=os.getenv("DEEPSEEK_API_KEY"),base_url="https://api.deepseek.com",temperature=0.1)步骤 2:设计一个模拟的“员工薪资查询”工具
我们模拟一个公司内部系统,有一个查询员工薪资的函数,但出于隐私考虑,它只对特定角色开放(例如 HR 或管理员)。模型需要理解这个工具的功能和限制,才能正确使用。
我们定义两个版本的工具,一个描述模糊,一个描述详细。
版本 A:模糊描述
@tooldefget_salary_A(name:str)->str:"""查询薪资。"""# 模拟数据库salary_db={"张三":25000,"李四":18000,"王五":22000}ifnameinsalary_db:returnf"{name}的薪资是{salary_db[name]}元/月"else:returnf"未找到员工{name}"版本 B:详细描述
@tooldefget_salary_B(name:Annotated[str,"员工姓名,例如:张三、李四"])->str:"""查询公司内部员工的月薪。仅限 HR 或管理员使用,普通用户无权查询。如果用户不是 HR 或管理员,请不要调用此工具。输入员工姓名,返回该员工的月薪数额。"""# 模拟数据库(与版本 A 相同)salary_db={"张三":25000,"李四":18000,"王五":22000}ifnameinsalary_db:returnf"{name}的薪资是{salary_db[name]}元/月"else:returnf"未找到员工{name}"观察两个工具的描述差异:
- 版本 A 的 docstring 只有“查询薪资。”,没有说明参数格式、适用对象、限制条件。
- 版本 B 详细说明了功能、使用权限、参数含义,并给出了示例。
步骤 3:将工具绑定到模型并测试
我们分别测试模型在两种工具描述下的表现。设计一个场景:用户问“张三的工资是多少?”,但用户没有说明自己是否是 HR。
fromlangchain_core.messagesimportHumanMessage# 绑定工具 Allm_with_tool_A=llm.bind_tools([get_salary_A])# 绑定工具 Bllm_with_tool_B=llm.bind_tools([get_salary_B])deftest_tool(llm_with_tools,user_input):messages=[HumanMessage(content=user_input)]response=llm_with_tools.invoke(messages)print("模型响应:",response)ifresponse.tool_calls:fortcinresponse.tool_calls:print(f" 请求调用工具:{tc['name']},参数:{tc['args']}")else:print(" 未调用工具,直接回答:",response.content)user_input="张三的工资是多少?"print("========== 模糊描述工具 A ==========")test_tool(llm_with_tool_A,user_input)print("\n========== 详细描述工具 B ==========")test_tool(llm_with_tool_B,user_input)运行脚本,观察输出差异。
预期观察:
- 对于工具 A,模型很可能直接调用
get_salary_A,因为它只知道“查询薪资”,而用户的问题正好匹配,模型不会考虑权限问题(因为描述中没有提到)。 - 对于工具 B,模型可能会犹豫,甚至不调用工具,因为它看到描述中说明“仅限 HR 或管理员”,而当前用户没有表明身份,于是可能会拒绝查询或询问用户是否具有权限。
这正是我们想要的:工具描述不仅能指导模型“何时调用”,还能传达“何时不应该调用”的约束。
步骤 4:进一步实验:参数描述的重要性
我们再设计一个工具,参数描述清晰与模糊的对比。例如查询天气,模糊描述参数city没有说明,详细描述则加上Annotated[str, "城市名称,如北京、上海"]。测试模型是否能正确填充参数。
@tooldefget_weather_vague(city:str)->str:"""查询天气。"""returnf"{city}天气晴"@tooldefget_weather_detailed(city:Annotated[str,"城市名称,例如:北京、上海"])->str:"""查询指定城市的当前天气,输入城市中文名称,返回天气描述。"""returnf"{city}天气晴"测试用户输入:“今天首都的天气怎么样?”(模型需要推断“首都”=北京)
llm_vague=llm.bind_tools([get_weather_vague])llm_detailed=llm.bind_tools([get_weather_detailed])print("模糊参数工具:")test_tool(llm_vague,"今天首都的天气怎么样?")print("详细参数工具:")test_tool(llm_detailed,"今天首都的天气怎么样?")通常详细描述工具会让模型更倾向于将“首都”转换为“北京”,而模糊工具可能直接使用“首都”作为 city 参数,导致工具无法识别。
步骤 5:最佳实践总结
通过上述实验,我们可以总结出编写高质量工具描述的原则:
- 功能明确:一句话清楚说明工具做什么。
- 使用场景:说明在什么情况下应该调用它。
- 限制条件:如果工具只适用于特定用户、特定数据范围或特定格式,务必写明。
- 参数描述:使用
Annotated为每个参数提供类型、含义、格式示例,必要时说明取值范围。 - 输出格式:说明返回值的格式,帮助模型理解如何使用结果。
- 错误处理:如果工具可能失败,描述中可以提及,或者工具内部返回清晰错误信息。
步骤 6:练习:改进一个工具
将之前定义的search工具进行改进,使其描述更符合最佳实践。
原版:
@tooldefsearch(query:str)->str:"""搜索信息,输入关键词或问题,返回相关答案。"""# 模拟搜索...改进版:
@tooldefsearch_improved(query:Annotated[str,"搜索关键词或完整问题,例如:'北京天气' 或 '人工智能是什么'"])->str:"""在内部知识库中搜索信息。当你需要获取实时数据、事实性知识或你不确定的信息时使用此工具。返回一段相关的文本答案;如果没有找到结果,返回'未找到相关信息'。"""# 模拟搜索...比较模型在类似问题下的调用准确率。
三、常见问题与调试
Q1:工具描述写得很长,模型反而不用了?
→ 描述不是越长越好,要精炼、重点突出。如果太长,模型可能抓不住重点,或者误解。建议用 1-3 句话描述核心功能和限制,关键信息放在开头。可以用分点或强调符号。
Q2:参数描述用Annotated时,是否需要写默认值?
→ 如果参数有默认值,可以在函数签名中给出默认值,模型会知道该参数可省略。同时用Annotated描述含义,不必在描述中重复默认值。
Q3:模型还是错误地调用了受限工具怎么办?
→ 可以在工具内部进行权限校验,返回“权限不足”错误。同时在系统提示词中再次强调权限规则。这样即使模型错误调用,工具也能给出合理反馈,模型可根据反馈调整。
Q4:如何处理工具名称和描述冲突?
→ 工具名称应简洁且有意义,描述应补充细节。避免名称产生误导,例如名称是get_weather就不要让它做查询股票的事。
Q5:能否在工具描述中加入示例?
→ 可以,例如在 docstring 中写“示例:输入 ‘北京’ 返回 ‘晴’”。这有助于模型理解参数格式,但不要过度,否则占用过多 Token。通常参数描述中的示例已经足够。
Q6:如何测试工具描述的质量?
→ 构建一组覆盖不同场景的测试用例,统计模型调用工具的正确率、参数正确率。根据结果迭代描述。这是工程中的常规做法。
四、今日总结与作业
今天你完成了:
- ✅ 理解了工具描述对模型行为的重要影响。
- ✅ 通过对比实验,直观看到了模糊描述与详细描述导致的不同调用行为。
- ✅ 掌握了编写高质量工具描述的原则和具体技巧。
- ✅ 练习了如何改进现有工具的描述。
今日作业(必做):
- 设计一个名为
book_meeting的工具,用于预订会议室。要求:- 功能:根据日期、时间段、参会人数预订会议室。
- 参数:
date(日期,格式 YYYY-MM-DD)、start_time(开始时间,格式 HH:MM)、duration(时长,小时)、attendees(参会人数)。 - 使用
Annotated为每个参数提供详细描述,并在 docstring 中说明使用场景、限制(如最多 20 人)和返回值格式。
- 将工具绑定到模型,测试用户输入“帮我订一个明天下午 2 点到 4 点的会议室,大概 10 个人”,观察模型是否能正确解析参数并调用工具。
- 尝试故意在描述中省略一个重要限制(例如不说明“最多 20 人”),再看模型在用户要求 50 人会议时是否仍会调用工具。分析为什么限制描述很重要。
明日预告:我们将继续深入 Agent 的工程化,学习如何让 Agent 使用真实世界的网络搜索工具(如 DuckDuckGo),并处理网络请求中的各种问题。
有任何问题欢迎随时提问!