🎯 课程主题
总结 LangChain 工具开发中的 5 条核心实践经验:清晰描述、功能单一、容错重试、返回字符串、同步/异步选择。
📝 核心知识点
1. 清晰的描述(Description & Docstring)
- 概念说明:为工具提供准确的
description和详细的参数说明(含默认值),帮助大模型正确理解和选择合适的工具。 - 关键细节:描述信息是大模型决策"是否调用该工具"以及"如何填充参数"的核心依据,模糊的描述会导致工具调用错误或遗漏。
2. 工具功能单一化
- 概念说明:一个工具只做一件事,遵循单一职责原则。
- 关键细节:若一个工具承担过多功能,大模型在调用时容易产生混淆,调用失败率上升。每个工具职责明确,大模型才能精准匹配。
3. 处理工具调用失败(三层容错)
- 概念说明:工具调用可能失败,需要从三个层面做容错处理。
- 关键细节:
- 工具内部级:在函数体内部添加异常处理(
try/except),确保工具不直接抛出异常,返回有意义的错误描述字符串。 - Agent 级重试:在 Agent 层面通过 Prompt 引导,失败后尝试其他替代方案(后续 Agent 课程展开)。
- 调用级重试:使用
@retry装饰器或手动实现重试机制(如tenacity库),设置重试次数(如retry=3,即 1 次正常调用 + 2 次重试)。 - 重试的价值:大模型存在幻觉问题,单次调用成功率可能只有 95%。多次重试可显著提升整体成功率(
1 - (1-0.95)^3 ≈ 99.99%),这也是大模型应用"耗 token"的原因之一。
- 工具内部级:在函数体内部添加异常处理(
4. 工具返回字符串而非字典
- 概念说明:工具返回值应为字符串类型,不要返回字典或其他数据结构。
- 关键细节:
- 大模型本质只处理文本,字符串是最友好的格式。
- 若返回字典,LangChain 会使用 Unicode 编码转换(如中文变
\u5f20\u4e09),模型理解效果远差于直接的字符串格式。 - 实际开发中应确保工具
return的是str类型。
5. 同步与异步的选择
- 概念说明:根据工具内部操作的耗时程度选择同步或异步调用。
- 关键细节:
- 简单计算、本地读取 → 同步调用(
def tool())。 - 频繁 I/O 操作(API 调用、数据库查询) → 异步调用(
async def tool()),提升并发性能。
- 简单计算、本地读取 → 同步调用(
🏗️ 架构与工作流
工具调用容错流程:
call_agent(user_input)
├── 成功 → 返回结果
└── 失败 → retry(agent.invoke)
├── 成功 → 返回结果
└── 失败 → retry(agent.invoke)
├── 成功 → 返回结果
└── 失败 → 抛出异常
💻 代码实战
from langchain_core.tools import tool
from tenacity import retry, stop_after_attempt, wait_fixed
import random
# ========== 1. 工具内部异常处理 ==========
@tool
def safe_divide(a: float, b: float) -> str:
"""安全的除法运算。
Args:
a: 被除数
b: 除数
"""
try:
result = a / b
return f"{a} / {b} = {result}"
except ZeroDivisionError:
return "错误:除数不能为零"
except Exception as e:
return f"计算出错:{str(e)}"
# ========== 2. 调用级重试示例 ==========
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
def call_agent_with_retry(agent, user_input: str) -> str:
"""带重试的 Agent 调用(模拟)。"""
# 模拟 5% 的失败概率
if random.random() < 0.05:
raise RuntimeError("模型调用失败(模拟)")
return f"回答:{user_input} 的处理结果"
# ========== 3. 返回字符串 vs 返回字典 ==========
# 推荐:返回字符串
@tool
def get_weather_good(city: str) -> str:
"""查询天气(推荐写法)"""
return f"{city}:晴天,25°C" # 直接返回字符串
# 避免:返回字典
@tool
def get_weather_bad(city: str) -> dict:
"""查询天气(不推荐的写法)"""
return {"city": city, "weather": "晴天", "temp": 25}
# 该字典会被转成 Unicode 编码传给模型,效果差
# ========== 4. 异步工具示例 ==========
@tool
async def fetch_news_async(keyword: str) -> str:
"""异步获取新闻(I/O 密集型场景)。"""
import asyncio
await asyncio.sleep(1) # 模拟网络请求
return f"关于「{keyword}」的最新新闻:..."
⚠️ 常见问题与避坑指南
- 工具返回值务必为
str类型,不要返回dict、list等复杂结构,否则会被 Unicode 编码导致模型理解困难。 - 重试次数不宜过多(通常 3 次足够),每次重试都会消耗 token,需权衡成功率和成本。
- 工具内部必须做异常处理,不要让异常直接抛出到调用层,否则会打断整个 Agent 执行链。
- 异步工具需配合异步的模型调用链路(
ainvoke),同步和异步混用会导致性能问题。
💡 个人总结与延伸
本课 5 条经验是 LangChain 工具开发的"黄金法则"。其中"返回字符串而非字典"和"函数单一职责"是最容易被忽视但影响最大的两个点。重试机制(tenacity 库)在生产环境中几乎是标配,但需合理设置 stop_after_attempt 和 wait 策略,避免无效重试浪费成本。这些实践在后期的 Agent 和 LangGraph 开发中同样适用。