跳转至

工具定义与使用

🛠️ 在 LangChain 中,如何用 @tool 装饰器快速定义一个工具?写出示例。

在 LangChain 中,@tool 装饰器是将普通 Python 函数转化为 Agent 可调用工具的最快方式。只需在函数定义前加上 @tool,并提供一个清晰的文档字符串(docstring),LangChain 就能自动提取工具的名称和描述。

核心代码示例:

from langchain.tools import tool

@tool
def search_weather(city: str) -> str:
    """查询指定城市的实时天气。输入城市名称,返回天气描述。"""
    # 模拟天气查询
    return f"{city}:晴,25摄氏度"

@tool
def calculate(expression: str) -> str:
    """执行数学计算。输入一个数学表达式(如 "2+2"),返回计算结果。"""
    try:
        result = eval(expression)
        return str(result)
    except Exception as e:
        return f"计算出错:{e}"

关键说明:

  • 函数名(如 search_weather)自动成为工具的 name 属性。

  • 文档字符串(docstring)自动成为工具的 description 属性。这是 Agent 理解工具功能的唯一途径,必须清晰、准确。

  • 参数类型提示(如 city: str)会被 LangChain 解析,并用于生成工具的参数 schema,尤其在 OpenAI Functions Agent 中,它能帮助 LLM 生成正确的函数调用 JSON。

高级用法:你也可以在 @tool 装饰器中显式指定名称或描述,以覆盖自动提取的值:

@tool(name="WeatherSearch", description="查询指定城市的天气,输入城市中文名称")
def search_weather(city: str) -> str:
    ...

但通常推荐直接写好函数名和 docstring,因为这样代码更简洁。

✅ 经验:我习惯将工具集中在一个 tools.py 文件中,每个工具函数只做一件事,docstring 以“做什么”开头,然后说明“输入什么,返回什么”。这样不仅 Agent 理解得好,同事维护起来也方便。


📝 工具的 name 和 description 字段为什么极其重要?它如何影响 Agent 的推理?

在 Agent 的 Prompt 中,工具的描述是 LLM 决定何时调用、调用哪个工具的唯一参考信息。LLM 看不到工具的代码实现,只能靠文字描述来理解工具的能力和适用范围。

name:是 Agent 在输出 Action 时使用的标识符。如果名称模糊(如 tool1),Agent 可能会选错或根本不知道该工具的存在。名称应该简洁、准确,通常是一个动词短语或名词,比如 SearchCalculatorWeatherQuery

description:是工具的灵魂。一个好的描述应该告诉 LLM:

  • 这个工具是干什么的?(核心功能)

  • 什么情况下应该使用它?(使用场景)

  • 输入参数是什么格式?(例如“输入城市的中文名称”)

  • 返回值是什么类型?(例如“返回天气描述字符串”)

描述对推理的影响:

  • 描述不清楚,Agent 会犹豫不决,或者乱猜。例如,描述只写“查询天气”,Agent 可能以为它只能查本地天气,于是每次都传入空字符串或错误参数。

  • 如果描述误导,Agent 会错误地选择工具。例如,把“计算”工具的描述写成“处理数学问题”,Agent 可能在遇到文字应用题时也调用它,期待工具能理解自然语言。

  • OpenAI Functions Agent 中,描述会被转换成函数 schema 的 description 字段,直接指导 LLM 生成正确的函数调用 JSON。如果描述不清晰,LLM 可能生成错误的参数名或类型,导致工具调用失败。

最佳实践:

  • 描述中明确工具能做什么,以及不能做什么。

  • 给出具体的输入示例。

  • 避免模糊词汇(如“处理”、“操作”)。

✅ 踩坑案例:我曾写过一个检索知识库的工具,描述是“搜索知识库”,结果 Agent 总是把整个用户问题直接丢进去搜索,而不是先提取关键信息。后来我把描述改成“从知识库中检索与特定关键词相关的文档片段。输入为一个简短的查询关键词,返回相关文档列表。”,Agent 的行为就正常了。


🔍 如果工具的 description 写得不清楚,Agent 会有什么表现?

工具描述模糊时,Agent 的表现会非常糟糕,典型的症状包括:

  1. 完全忽略该工具:Agent 在推理过程中认为没有可用的工具能解决当前问题,即使答案就在那个工具的背后。例如,一个“查订单”的工具描述为“获取信息”,Agent 可能会去调用另一个更明确的“搜索FAQ”工具。

  2. 错误地调用工具:Agent 可能会把本不属于该工具的任务交给它。比如,把“计算”工具的描述写成“处理数学”,当用户问“什么是勾股定理”时,Agent 可能也会调用它,期待工具能返回定义。

  3. 调用时参数不正确:描述没说明参数格式,Agent 就会随意传入参数。例如工具期望数字 ID,Agent 却传入了整个问题字符串,导致工具返回错误。

  4. 在循环中反复尝试:工具调用失败后,Agent 可能不认为是工具描述的问题,而是认为是自己的参数不对,于是不断改变参数重试,陷入死循环。

  5. 输出不相关的结果:工具返回了数据,但因为 Agent 没用对,得到的结果是垃圾,它可能会基于这些垃圾数据继续推理,输出完全错误的答案。

总结:工具描述是 Agent 理解外部世界的唯一说明书。描述模糊,Agent 就像蒙着眼睛找东西,效率极低。

✅ 调试技巧:如果发现 Agent 总是用错工具或参数,首先检查工具的 description。可以临时开启 verbose=True,观察 Agent 的“Thought”,看它想做什么,再对比工具的 description,往往就能发现不匹配之处。


🏷️ 你如何为工具的参数添加类型提示和描述?这有什么好处?

在 LangChain 中,为工具参数添加类型提示(Type Hints) 和文档字符串(Docstring) 是对工具进行声明式配置的最佳方式。

如何添加:

  • 类型提示:直接使用 Python 标准类型(如 str, int, bool)或 typing 模块的类型(如 List[str])。LangChain 会自动解析这些类型并生成 JSON Schema,供 OpenAI Functions Agent 等使用。

  • 参数描述:在函数的 docstring 中,使用 Args: 区块来描述每个参数的含义和格式。对于更复杂的 schema,还可以使用 PydanticField@tool 装饰器的 args_schema 参数来明确定义。

示例:

from langchain.tools import tool

@tool
def send_email(to: str, subject: str, body: str, cc: list[str] = None) -> str:
    """发送邮件。
    Args:
        to: 收件人邮箱地址
        subject: 邮件主题
        body: 邮件正文
        cc: 抄送邮箱列表,可选
    """
    # 实际发送逻辑
    return "邮件发送成功"

好处:

  1. 精确指导 LLM:在 OpenAI Functions Agent 中,类型提示和描述会被转换为函数的 parameters schema,LLM 据此生成精确的 JSON 参数。这避免了 LLM 胡乱猜测参数格式。

  2. 自动生成工具 Schema:LangChain 可以自动从这些信息生成 ToolInputSchema,省去手动编写繁琐的 Pydantic 模型。

  3. 提升稳定性:类型提示让 Agent 知道参数是字符串还是整数,减少因类型错误导致的工具调用失败。

  4. 更好的开发体验:IDE 能提供自动补全和类型检查,减少低级错误。

✅ 心得:对于复杂参数,我推荐使用 PydanticBaseModel 定义 args_schema,然后传入 @tool(args_schema=...)。这样既能享受 IDE 的强类型支持,又能生成完整的 JSON Schema,Agent 理解得最准确。


🔌 当工具需要访问外部资源(如数据库连接),你如何管理这些资源的生命周期?

工具函数在执行时,往往需要数据库连接、网络会话、文件句柄等外部资源。管理这些资源的生命周期,核心是避免重复创建、确保正确关闭、以及在分布式环境中安全共享。

常用策略:

  1. 使用全局变量或单例模式(简单场景)

将数据库连接池或客户端实例化为全局变量,工具函数直接引用。优点是简单,缺点是资源会在模块加载时就创建,且测试时难以替换。

# 全局连接池,应用启动时初始化
db_pool = create_db_pool()

@tool
def query_db(sql: str) -> str:
    with db_pool.get_connection() as conn:
        return conn.execute(sql)
  1. 通过闭包或类封装(推荐)

将资源作为外部变量注入到工具函数中,实现工具与资源的解耦。这便于单元测试(可注入 mock)和资源复用。

def make_query_tool(db_pool):
    @tool
    def query_db(sql: str) -> str:
        with db_pool.get_connection() as conn:
            return conn.execute(sql)
    return query_db

# 初始化
pool = create_db_pool()
query_tool = make_query_tool(pool)
  1. 利用 toolreturn_direct 和上下文管理器 对于需要每次调用都初始化和清理的轻量资源,可以在函数内部使用 with 语句。对于重量级资源,务必在函数外部复用。

  2. AgentExecutor 级别管理生命周期 通过自定义 CallbackHandler,在 on_agent_start 时创建资源,在 on_agent_finish 时销毁资源。但这会增加复杂度。

  3. 使用依赖注入框架 在复杂的应用中,可以引入依赖注入容器(如 dependency-injector),统一管理数据库、缓存等资源的生命周期。

✅ 实践原则:

  • 数据库连接、HTTP 客户端等昂贵资源必须复用,使用连接池。

  • 文件句柄、临时缓存等应在使用后立即关闭。

  • 永远不要在工具函数内部直接硬编码资源创建逻辑,否则测试和移植会非常痛苦。我通常采用“工厂函数”模式,将资源作为参数传入,生成工具。


⚠️ 什么是工具的“错误处理”?如果工具执行失败,应该返回什么?

工具的错误处理是指当工具执行过程中发生异常(如网络超时、数据库宕机、参数非法)时,如何优雅地响应,而不是让整个 Agent 崩溃。

核心原则:工具应始终返回一个字符串信息,而非抛出未捕获的异常。 因为 Agent 依赖工具的返回值(Observation)来推理下一步。如果工具抛出异常,LangChain 默认会中断整个 Agent 执行,用户将得到一个冰冷的错误。

错误处理的最佳实践:

  1. 在工具函数内部使用 try...except 捕获所有异常,并返回一个描述清晰的错误信息字符串。例如:
@tool
def risky_tool(param: str) -> str:
    try:
        result = perform_network_call(param)
        return result
    except TimeoutError:
        return "错误:网络请求超时,请稍后重试。"
    except ValueError as e:
        return f"错误:参数无效 - {e}"
    except Exception as e:
        return f"错误:工具执行失败,原因不明 - {e}"
  1. 错误信息要具体:告诉 Agent 发生了什么,以及可能的解决方法。例如“API 密钥无效,请检查配置”,而不是简单的“失败”。这样 Agent 可以据此调整行为(比如不再重试,或者向用户询问密钥)。

  2. 区分可恢复错误与致命错误:对于临时性错误(如超时),可以提示 Agent “请稍后重试”;对于永久性错误(如权限不足),应明确告知“操作被拒绝”。

  3. 自定义错误处理回调:使用 on_tool_error 回调,可以在全局统一处理工具错误,例如发送告警、记录日志等。

✅ 教训:早期我把工具写得很“脆”,一遇到问题就抛异常,结果 Agent 经常突然死亡,用户体验极差。后来所有工具都加上健壮的 try...except,并返回结构化错误信息(如 JSON),Agent 的执行稳定性大幅提升。


⏳ 如何处理工具的异步调用?在 @tool 中如何定义异步函数?

LangChain 支持异步工具,这在需要并发调用多个 API、执行 I/O 密集型任务时能显著提升效率。

定义异步工具: 只需将工具函数定义为 async def,并使用 @tool 装饰器即可。LangChain 会自动识别并支持异步调用。

import aiohttp
import asyncio

@tool
async def fetch_web_content(url: str) -> str:
    """异步获取网页内容。输入URL,返回网页文本。"""
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

在 Agent 中使用异步工具:

  • 使用 AgentExecutor 的异步方法 arunainvoke,它会自动以异步方式调用工具。

  • 如果多个工具可并行执行(如在 OpenAI Functions Agent 中),Agent 会一次性发起多个工具调用,然后等待所有结果返回,这能大幅缩短总执行时间。

注意事项:

  • 异步工具内部使用的库必须是异步兼容的(如 aiohttp 代替 requestsaiomysql 代替 mysql-connector)。

  • 如果工具是同步函数,Agent 会在默认线程池中执行它们,这可能成为并发瓶颈。因此对于 I/O 密集型工具,强烈建议实现为异步。

  • 调试异步工具时,可以使用 asyncio.run() 在同步环境中测试。

✅ 使用场景:我有一个竞品分析 Agent,需要同时抓取多个网站的价格信息。将所有抓取工具改为异步后,Agent 的执行时间从 8 秒降到了 1.5 秒,因为多个请求完全并行。


🧠 如何定义多个工具并让 Agent 在运行时选择?Agent 是根据什么信息选择的?

定义多个工具:只需创建一个工具列表,然后传递给 Agent 的初始化函数或 AgentExecutor。例如:

tools = [search_weather, calculate, send_email]
agent = create_openai_functions_agent(llm, tools, prompt)

Agent 在运行时,会根据当前对话上下文和工具的描述来决策。具体来说,LangChain 会将所有工具的名称、描述和参数 schema 格式化后注入 Prompt。LLM 阅读用户的问题,然后决定是否需要调用工具、调用哪个工具、以及传入什么参数。

Agent 的选择依据(按重要性排序):

  1. 工具的 description:告诉 LLM 该工具能解决什么问题。这是最关键的。

  2. 用户当前的提问:LLM 会分析用户意图,与工具描述进行匹配。

  3. 先前的交互历史(如果使用了 Memory):Agent 可能根据之前的对话结果选择工具,比如“刚才查询了天气,现在需要推荐活动”。

  4. 工具的参数 schema:LLM 会判断它是否能从用户问题或上下文中提取出正确的参数。如果参数缺失,它可能会主动向用户询问。

示例:用户问“明天杭州天气如何?”,Agent 读到 search_weather 的描述“查询指定城市的实时天气”,而 calculate 的描述是“执行数学计算”,它会自然地选择前者。

✅ 技巧:如果 Agent 经常选错工具,问题通常出在工具的描述重叠。例如,两个工具都能“获取信息”,Agent 就会随机选。此时需要让描述更具区分度,比如“从数据库查询订单”和“从网页搜索新闻”。


❓ 如果工具的输入需要多步确认(如“确定要删除吗?”),这在 LangChain 中怎么实现?

需要用户确认的操作(如删除数据、支付)不能单靠工具函数本身,因为 Agent 默认是自动执行的,不会中途暂停等待用户输入。实现多步确认有以下几种方式:

方案一:工具返回“确认请求”,由 Agent 询问用户(最自然) 工具不执行实际操作,而是返回一个特殊的标记,比如 "CONFIRM_REQUIRED: 确定要删除订单 #12345 吗?请回复 Y/N"。Agent 看到这个返回后,应该在 Thought 中理解这需要用户确认,然后将这个确认请求作为 Final Answer 返回给用户。下一轮对话时,用户的确认(“Y”)会作为新的输入,Agent 再次调用该工具,并附上确认参数。

@tool
def delete_order(order_id: str, confirmed: bool = False) -> str:
    """删除订单。首次调用时请将 confirmed 设为 False 以请求确认。"""
    if not confirmed:
        return f"CONFIRM_REQUIRED: 确定要删除订单 {order_id} 吗?此操作不可撤销。"
    # 实际删除逻辑
    return f"订单 {order_id} 已删除"

方案二:使用 Human-in-the-Loop 工具 LangChain 提供了 HumanInputToolInputTool,可以在工具执行时暂停并等待人工输入。但这种方法需要特殊的运行环境(如 Jupyter Notebook)。

方案三:在 Agent 外部增加确认拦截器 在 AgentExecutor 和用户界面之间加一层逻辑:当 Agent 返回的 Final Answer 中包含 CONFIRM_REQUIRED 时,UI 渲染确认按钮,用户点击后,再发起新的一轮对话(附带确认信息)。

方案四:利用回调中断

在工具执行前的回调中,根据条件暂停执行,但实现起来较复杂。

✅ 实践建议:我倾向于方案一,因为它最符合 Agent 的自然交互模式。它利用 Agent 的推理能力来处理确认流程,而不是依赖外部机制。关键是工具设计时要支持“两段式”调用。


🔐 你如何限制某个工具只能被特定 Agent 或特定用户使用?

在多 Agent 系统或多租户应用中,工具权限控制至关重要。LangChain 本身不提供内置的权限管理,但你可以通过以下几种方式实现:

  1. 在工具函数内部进行权限校验

最简单的做法。工具函数接收用户信息(如通过全局上下文或参数),在执行实际操作前检查权限。如果没有权限,返回一个明确的错误信息。

@tool
def admin_tool(user_id: str, action: str) -> str:
    """执行管理员操作。"""
    if not is_admin(user_id):
        return "错误:权限不足,只有管理员可以执行此操作。"
    # 执行管理操作
  1. 在 Agent 初始化时动态过滤工具列表

根据当前用户或 Agent 的角色,创建不同的工具列表。这样 Agent 在 Prompt 中根本看不到无权使用的工具,自然无法调用。

def build_tools_for_user(user):
    base_tools = [search, calculate]
    if user.is_admin:
        base_tools.append(admin_tool)
    return base_tools
  1. 使用自定义的 AgentExecutor 包装器 继承 AgentExecutor,重写 _take_next_step 方法,在执行工具前检查权限。这可以实现更细粒度的控制,比如记录审计日志。

  2. 通过工具注册中心和元数据管理 创建一个工具注册表,每个工具有 required_role 属性。在 Agent 启动时,只加载与当前用户角色匹配的工具。

✅ 我的做法:我通常采用“动态工具列表” + “工具内部兜底检查”的双重保险。动态列表防止 Agent 看到不该看的工具(更安全),工具内部检查则作为最后防线,防止代码 bug 导致越权。权限信息通过全局上下文注入,避免每个工具函数都手动传递用户ID。


🌐 在 Agent 中,如何传入调用工具时需要的全局上下文(如 user_id)?

许多工具在执行时需要知道当前的用户身份、会话ID、请求来源等全局上下文。这些信息不应由 Agent 自己生成,而应可靠地从外部注入。

方法一:通过闭包(工厂函数)注入

在创建工具时,将上下文绑定到函数闭包中。这是最推荐的方式,因为它让工具函数保持纯净,上下文作为外部依赖显式传入。

def make_weather_tool(user_id):
    @tool
    def weather(city: str) -> str:
        # 使用 user_id 记录日志、鉴权等
        log_query(user_id, city)
        return get_weather(city)
    return weather

# 在请求处理中
user_tool = make_weather_tool(current_user.id)
agent_executor = AgentExecutor(agent=agent, tools=[user_tool])

方法二:使用 partialtoolargs_schema 注入 不推荐让 Agent 自己传递 user_id,因为 LLM 可能会遗忘或出错。更好的方式是将 user_id 作为工具函数的非 LLM 可见参数,在调用时自动填充。这通常需要自定义 AgentExecutor 或使用 LangChain 的 RunnableConfig 来传递。

方法三:全局请求上下文(如 contextvars) 在异步 Web 框架(如 FastAPI)中,可以使用 Python 的 contextvars 将请求级上下文(如 user_id)存储在线程/协程本地变量中,工具函数内部直接读取。这避免了参数传递,但要注意协程安全。

import contextvars
current_user_id = contextvars.ContextVar("user_id")

@tool
def my_tool(query: str) -> str:
    uid = current_user_id.get()
    # 使用 uid

方法四:通过 Agent 的 Memory 传递

将用户信息存储在 Memory 中,但这不适用于安全敏感信息,因为 LLM 可能会无意泄露。

✅ 推荐方案:对于简单应用,用闭包注入最直接;对于复杂的 Web 应用,用 contextvars 可以极大简化代码。无论如何,绝不要让 LLM 自己生成或传递 user_id,这既不安全也不可靠。

⚡ 什么是工具的“coroutine”?在哪些场景下需要返回协程?

在 LangChain 中,工具的 coroutine(协程) 指的是将工具函数定义为 async def 异步函数。这使得工具在执行时不会阻塞事件循环,能够在等待 I/O 操作(如网络请求、数据库查询)时让出控制权,从而提升并发性能。

定义方式:只需将函数改为 async def,并配合异步库使用。

@tool
async def fetch_price(product: str) -> str:
    """异步查询商品价格。输入商品名称,返回当前价格。"""
    async with aiohttp.ClientSession() as session:
        async with session.get(f"https://api.example.com/price/{product}") as resp:
            data = await resp.json()
            return f"{product} 现价 {data['price']} 元"

需要返回协程的场景:

  • 高并发 I/O 操作:当 Agent 需要同时调用多个工具(如查询多个数据源),异步工具能大幅缩短总等待时间。LangChain 的 AgentExecutor 使用 arun 时会并行调度这些协程。

  • 长时间运行但计算不密集的任务:比如等待外部 API 的缓慢响应。如果用同步工具,整个 Agent 推理线程都会被阻塞,影响系统吞吐量。

  • 与异步 Web 框架集成:如果你的应用基于 FastAPI 等异步框架,使用异步工具可以避免阻塞事件循环,保持服务的响应性。

  • 流式处理与超时控制:协程更容易与 asyncio.wait_for 结合实现超时,也能在等待期间执行其他逻辑。

在 Agent 中的使用:当调用 agent_executor.arun() 时,LangChain 会自动检测工具是否为协程,并以 await 方式调用。对于同步工具,会通过 asyncio.to_thread 在线程池中执行,但异步工具更高效。

✅ 实践经验:在构建一个需要同时抓取多个网站数据的 Agent 时,将所有抓取工具改为异步后,执行时间从 8 秒降至 1.5 秒。但要注意,异步工具必须使用异步库(如 aiohttp),如果内部使用了同步库,协程优势将消失。


🧪 你如何测试单个工具的功能是否正确?会 mock 外部依赖吗?

测试工具是保证 Agent 可靠性的基础。我会编写单元测试和集成测试,对于外部依赖一律采用 mock 手段。

单元测试(推荐使用 pytest):

  • 对于工具函数的核心逻辑(如数据处理、条件判断),直接调用函数并断言返回值。

  • 对于依赖外部资源的部分(数据库、API、文件系统),使用 unittest.mockpytest-mock 来模拟,确保测试快速、可重复,不受外部环境影响。

示例:测试一个调用外部 API 的天气工具。

from unittest.mock import patch, MagicMock

def test_weather_tool_success():
    mock_response = MagicMock()
    mock_response.json.return_value = {"weather": "晴", "temp": 25}
    with patch('requests.get', return_value=mock_response):
        result = weather_tool("北京")
        assert "晴" in result
        assert "25" in result

def test_weather_tool_timeout():
    with patch('requests.get', side_effect=requests.exceptions.Timeout):
        result = weather_tool("上海")
        assert "超时" in result  # 工具内部应捕获异常并返回友好信息

集成测试:在隔离的测试环境中(如本地数据库、测试专用的 API Key)运行工具,验证真实交互。这不作为每次提交的必测项,但发布前必须执行。

Mock 外部依赖的好处:

  • 速度快:不依赖网络和数据库,毫秒级完成。

  • 稳定:不会因外部服务故障导致测试失败。

  • 可控:可以模拟各种异常情况(超时、权限错误、返回数据异常),验证工具的健壮性。

✅ 我的习惯:所有涉及 I/O 的工具都必须有 100% 的单元测试覆盖,并且至少要包含成功、失败、超时三种场景的 mock。集成测试则放在 CI/CD 的晚间构建中。


📦 工具返回的结果如果很大(如整个网页的内容),你如何处理?

当工具返回的内容过大时,会引发 Prompt 超限、LLM 处理缓慢、成本飙升等一系列问题。处理原则是:永不将原始的大内容直接返回给 Agent,必须在工具内部进行压缩或分段。

处理方法:

  1. 提取摘要:使用 LLM 或规则对内容进行总结,只返回关键信息。例如,网页抓取工具只返回标题、前几段和重要链接。

  2. 分页返回:对于数据库查询等,实施分页逻辑,每次只返回前 N 条记录,并提供“下一页”的指示。但 Agent 需要能够逐步请求。

  3. 存储到外部,返回引用:将完整内容保存到文件系统、对象存储或向量数据库,然后只返回一个引用 ID 和简短描述。后续如需详细分析,Agent 可通过另一个工具传入 ID 获取详情。

  4. 截断并给出提示:对结果字符串进行硬截断(如前 2000 字符),并在末尾添加 ...(内容已截断,完整内容长度XXXX字)。这样 Agent 知道信息不完整,可决定是否发起更具体的查询。

  5. 使用 LangChain 的 Document 压缩器:在工具外部,可以借助 ContextualCompressionRetriever 对返回结果进行后处理,但这更多用于检索链。

设计工具时就要考虑大小限制:在工具的描述中,可以写明“返回最多前 10 条结果”,或“返回摘要而非全文”,让 Agent 自身也知晓这一限制。

✅ 教训:我曾直接让一个网页抓取工具返回整个 HTML,结果 Agent 疯狂失败。后来改为只提取正文文本并限制 1500 字,Agent 才恢复正常。记住:Agent 的上下文窗口是稀缺资源,工具的返回值应像“电报”一样精炼。


🖼️ 工具可以返回图片、文件等非文本内容吗?LangChain 支持多模态工具吗?

工具返回值通常是字符串,因为 Agent 的推理过程以文本为基础。但是,我们可以通过一些技巧让工具“返回”图片、文件等非文本内容,LangChain 也在逐步扩展对多模态的支持。

当前可行的实现方式:

  • 图片:工具可以将图片保存到临时存储(如云存储),然后返回图片的 URL 或 Base64 编码字符串。如果 LLM 是多模态的(如 GPT‑4V),Agent 可以通过 URL 或 Base64 在 Prompt 中直接引用图片内容。

  • 文件:工具生成文件后,返回一个下载链接或文件路径。Agent 可以告诉用户“文件已生成,可点击下载”。

  • 音频、视频:同理,返回资源的 URL 和元数据。

LangChain 的多模态工具支持:

  • LangChain 的模型组件已支持多模态输入(如图像),但目前工具系统本身仍以文本为交互媒介。工具无法直接返回二进制流,但可以通过返回 URL 让 LLM 进行多模态理解。

  • 对于需要将图片直接作为工具输出并传递给 LLM 的场景,可以通过自定义 Agent 或修改 Prompt,在消息中直接插入 {"type": "image_url", "image_url": ...} 这样的内容块。

未来趋势:随着多模态模型和 API 的发展,LangChain 很可能会引入原生的多模态工具返回类型,允许工具直接输出图像、文件等复杂对象。

✅ 当前建议:如果你的工具涉及图片生成,使用返回 URL 的方式最通用。对于需要多模态理解的场景,选择支持多模态的 LLM(如 gpt-4-vision-preview),并在 Agent 的 Prompt 中显式说明工具返回的是图片 URL。


👤 你如何在 LangChain 中实现一个“人工审批”工具?即工具调用时需要人工介入。

人工审批工具的核心是在 Agent 自动执行流程中插入一个“暂停点”,等待人类确认后继续。实现方式通常利用工具的两段式调用和Agent 的自然交互能力。

实现方法:

  1. 设计双模式工具:工具函数接受一个额外的 confirmed 参数,默认为 False。首次调用时,不执行实际操作,而是返回一个 CONFIRM_REQUIRED 消息。Agent 会将该消息作为最终回答呈现给用户。用户确认后,再次调用同一工具并传入 confirmed=True,工具才会执行实际操作。

  2. 在工具描述中说明机制:确保 Agent 知道这个工具需要确认,避免它忽略返回的确认请求。

示例代码:

@tool
def delete_order(order_id: str, confirmed: bool = False) -> str:
    """删除订单。调用此工具时,如果未确认,会要求用户确认;确认后再次调用即可执行删除。
    Args:
        order_id: 订单号
        confirmed: 是否已经用户确认,默认为False
    """
    if not confirmed:
        return f"CONFIRM_REQUIRED: 确定要删除订单 {order_id} 吗?请回复 Y/N 确认。"
    # 执行删除
    return f"订单 {order_id} 已成功删除"w

执行流程:

  • Agent 想要删除订单,调用 delete_order("12345") → 返回 CONFIRM_REQUIRED: ... → Agent 向用户展示此消息。

  • 用户回复“Y”,新一轮对话开始,Agent 再次调用 delete_order("12345", confirmed=True) → 执行删除。

高级方案:如果希望直接在工具内部暂停并获取用户输入,可以使用 HumanInputRun 工具,但这会阻塞 Agent 事件循环,不适用于异步环境。

✅ 实践要点:确认信息必须清晰、明确,工具名称和描述应体现审批特性。对于高风险操作,我还建议在服务端增加二次校验,不单单依赖 Agent 逻辑。


🧰 解释“Toolkit”的概念:如何将一组相关的工具打包成一个 Toolkit?

Toolkit 是 LangChain 中用于组织和管理一组相关工具的容器。它将具有共同上下文(如数据库连接、API 配置)的工具打包在一起,提供统一的初始化接口。

为什么需要 Toolkit?

  • 共享资源:比如数据库连接池,多个工具(查询、更新、描述表)可以共享同一个连接,避免重复创建。

  • 简化配置:用户只需配置 Toolkit,而不必逐个创建和配置每个工具。

  • 逻辑内聚:将属于同一领域的工具放在一起,代码结构更清晰。

如何实现一个 Toolkit:继承 BaseToolkit,在 get_tools() 方法中返回工具列表。通常在 init 中初始化共享资源。

示例:一个简单的数学工具包。

from langchain.tools import BaseToolkit, BaseTool

class MathToolkit(BaseToolkit):
    def get_tools(self) -> list[BaseTool]:
        return [
            add_tool,
            multiply_tool,
            sqrt_tool,
        ]

在初始化时,可以传入共享的配置。例如 SQL 工具包会传入 db 连接对象,所有工具都使用它。

与普通工具列表的区别:普通工具列表是扁平的,而 Toolkit 提供了封装。你可以将 Toolkit 的 get_tools() 结果与其他工具混合使用。

✅ 使用经验:当工具需要共享状态或配置时,我必定使用 Toolkit。这不仅让代码更整洁,也便于单元测试(只需 mock 一个 Toolkit 即可替换所有相关工具)。


🗄️ 自己设计一个 SQL Toolkit,让它能执行查询并解释结果。

设计一个 SQL Toolkit,需要包含至少三个工具:sql_query(执行查询)、sql_describe_table(获取表结构)、sql_explain(解释查询结果)。它们共享一个数据库连接对象。

示例实现:

from langchain.tools import BaseToolkit, tool
from sqlalchemy import create_engine, text

class SQLToolkit(BaseToolkit):
    def __init__(self, db_url: str):
        self.engine = create_engine(db_url)

    def get_tools(self):
        @tool
        def sql_query(query: str) -> str:
            """执行 SQL 查询并返回结果。输入有效的 SQL SELECT 语句,返回结果表格。"""
            with self.engine.connect() as conn:
                result = conn.execute(text(query))
                rows = result.fetchmany(20)  # 限制返回行数
                return str(rows)

        @tool
        def sql_describe_table(table_name: str) -> str:
            """获取指定表的列信息。输入表名,返回列名和数据类型。"""
            with self.engine.connect() as conn:
                result = conn.execute(text(f"DESCRIBE {table_name}"))
                return str(result.fetchall())

        @tool
        def sql_explain(query: str) -> str:
            """解释 SQL 查询的执行计划。输入查询语句,返回执行计划说明。"""
            with self.engine.connect() as conn:
                result = conn.execute(text(f"EXPLAIN {query}"))
                return str(result.fetchall())

        return [sql_query, sql_describe_table, sql_explain]

使用:创建 Toolkit 时传入数据库 URL,然后通过 get_tools() 获取工具列表,传给 Agent。

增强:还可以添加一个工具,利用 LLM 对查询结果进行自然语言解释,例如“查询结果显示了最近一周的销售趋势...”。

✅ 注意:安全至关重要。必须限制工具只执行 SELECT 语句,并在工具内部进行 SQL 校验,防止注入和误操作。生产环境务必使用只读数据库账户。


💸 如果 Agent 对某个工具的使用频率过高,造成成本上升,你如何限制?

限制工具使用频率是控制成本和资源消耗的必要手段。可以采用多层防御:

  1. 工具内部实现速率限制(Rate Limiting):使用令牌桶或滑动窗口算法,记录每次调用,当超过阈值时返回错误提示。例如:
from functools import wraps
import time

def rate_limited(max_calls, period):
    calls = []
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            calls[:] = [c for c in calls if c > now - period]
            if len(calls) >= max_calls:
                return f"错误:该工具每分钟最多调用 {max_calls} 次,请稍后再试。"
            calls.append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@tool
@rate_limited(max_calls=5, period=60)
def expensive_search(query: str) -> str:
    ...
  1. 在 Agent 层面控制:设置 max_iterations 限制总步数,间接减少工具调用次数。

  2. 利用回调监控和告警:通过 on_tool_end 回调记录每个工具的调用次数,超过预设阈值时发送告警,或动态调整工具描述让 Agent 降低其使用优先级。

  3. 经济激励设计:在工具描述中加入“此工具调用成本较高,请谨慎使用”,引导 LLM 选择更便宜的替代工具。

  4. 运行时动态禁用:编写自定义 AgentExecutor,在工具调用次数超限后,临时从工具列表中移除该工具。

✅ 实际应用:我有一个调用外部付费 API 的工具,通过装饰器实现了每分钟最多 10 次的限制,并在工具描述中明确写明“有调用频率限制”,Agent 会自发地减少不必要的调用。


🔮 你认为 LangChain 的工具系统有哪些可以改进的地方?

尽管 LangChain 的工具系统极大简化了 Agent 开发,但在实际使用中仍有一些不足:

  1. 工具描述依赖自然语言,缺乏结构化元数据:目前工具的能力全靠文字描述,容易产生歧义。如果能引入声明式的元数据(如工具分类、适用场景标签、成本级别等),Agent 的选择会更精准。

  2. 异步和同步切换不够灵活:部分组件对异步工具的支持仍不完善,有时需要开发者自己实现兼容层。

  3. 缺少内建的权限控制和审计:没有原生的工具调用权限管理,多用户场景下需要开发者手动实现,增加了安全风险。

  4. 多模态支持有限:工具无法直接返回图片、音频等非文本内容,阻碍了多模态 Agent 的发展。

  5. 工具结果缓存机制缺失:相同输入的高频工具调用(如天气查询)无法自动缓存,导致重复计算和浪费。

  6. 工具热更新和版本管理不足:在长时间运行的服务中,无法平滑更新工具定义或替换工具版本。

  7. 工具错误恢复和重试策略不够智能:默认情况下,工具失败只会返回错误文本,缺乏自动重试或降级策略。

改进方向:

  • 为工具增加结构化配置文件(如 YAML/JSON),支持声明式定义。

  • 提供原生缓存装饰器,可接入 Redis 等后端。

  • 开发工具注册与发现中心,支持动态加载和卸载。

  • 集成权限框架,支持基于角色的访问控制。

✅ 愿景:我希望未来的工具系统能像“函数即服务”一样,具有完整的生命周期管理、可观测性和安全策略,让 Agent 不仅可以调用工具,还能信任工具、管理工具。