logo
0
0
WeChat Login

LangChain Tool Calling Demo

这是一个演示项目,展示如何使用 LangChain 实现 LLM 工具调用(Tool Calling),以及如何在工具执行时传递用户鉴权信息等额外参数。

这个 Demo 解决什么问题?

当 LLM 需要调用外部 API 时,外部服务通常需要知道"是谁在调用"——比如查询用户自己创建的数据库列表,后端服务需要用户的 Cookie 或 Token 来鉴权。但 LLM 本身不会也不应该感知这些鉴权信息。

本项目演示了一套完整的解决方案:通过 LangChain 的 RunnableConfig 机制,将用户上下文透传到工具函数中

核心原理

数据流

浏览器(带 Cookie) → FastAPI 路由(提取 request.headers["cookie"]) → LangChain Engine(注入 RunnableConfig.configurable) → @tool 函数(从 config 中读取 cookie,附加到 HTTP 请求头) → 外部服务(鉴权通过 ✅)

关键代码说明

1. 定义工具 — 通过 RunnableConfig 接收额外参数

app/tools.py 中,工具函数通过声明 config: RunnableConfig 参数来接收运行时上下文:

from langchain_core.runnables import RunnableConfig from langchain_core.tools import tool @tool async def list_user_databases( page: int = 1, page_size: int = 20, *, config: RunnableConfig, # ← LangChain 自动注入,不会暴露给 LLM ) -> str: """获取当前用户创建的数据库列表。""" # 从 config 中提取用户鉴权信息 user_cookie = config.get("configurable", {}).get("user_cookie", "") # 将鉴权信息透传到外部服务请求中 headers = {"Cookie": user_cookie} async with httpx.AsyncClient() as client: resp = await client.get("http://external-service/api/databases", headers=headers) return resp.text

⚠️ 重要config 参数的类型必须是 RunnableConfig(不能是 Optional[RunnableConfig])。 LangChain 通过参数类型注解来识别并注入 config,如果用了 Optional 包装,LangChain 将无法识别,config 会是 None

2. 调用工具时注入用户上下文

app/engine.py 中,调用 LLM 和执行工具时,通过 config 参数传递用户信息:

from langchain_core.runnables import RunnableConfig # 构建包含用户信息的 config config: RunnableConfig = { "configurable": { "user_cookie": user_cookie, # 从 HTTP 请求头中提取 "user_token": user_token, # 也可以传递 Bearer Token } } # 调用 LLM 时传入 config(LLM 本身不使用,但会透传给工具) response = await llm_with_tools.ainvoke(messages, config=config) # 执行工具时传入同一个 config → 工具函数就能拿到 user_cookie tool_result = await tool_func.ainvoke(tool_args, config=config)

3. 路由层提取用户原始请求信息

app/routes_chat.py 中,从 FastAPI 的 Request 对象提取浏览器发来的 Cookie:

@router.post("/stream") async def stream_message(req: SendMessageRequest, request: Request, ...): # 提取用户浏览器发来的原始 Cookie 字符串 user_cookie = request.headers.get("cookie", "") # 传递给聊天引擎,最终会到达工具函数 async for event in chat_stream( db=db, session_id=req.session_id, user_message=req.message, user_cookie=user_cookie, # ← 透传 ): yield f"data: {json.dumps(event)}\n\n"

工具调用循环流程

LLM 的工具调用不是一次完成的,而是一个多轮循环

用户: "帮我查一下我创建了哪些数据库" [第 1 轮] LLM 返回 tool_calls: [{name: "list_user_databases", args: {}}] → 执行工具,得到 JSON 结果 → 将结果作为 ToolMessage 加入消息列表 [第 2 轮] LLM 收到工具结果,生成最终文本回复 → "您目前创建了 5 个数据库,分别是..."

本项目使用 astream 流式接口统一处理工具调用和文本输出,避免重复调用 LLM。

如何扩展自己的工具

app/tools.py 中添加:

@tool async def my_new_tool( param1: str, param2: int = 10, *, config: RunnableConfig, ) -> str: """工具描述(LLM 会根据这段描述决定何时调用此工具)。""" user_cookie = config.get("configurable", {}).get("user_cookie", "") async with httpx.AsyncClient() as client: resp = await client.get( "https://your-service.com/api/xxx", headers={"Cookie": user_cookie}, params={"param1": param1, "param2": param2}, ) return resp.text

然后添加到 ALL_TOOLS 列表:

ALL_TOOLS = [ list_user_databases, get_database_detail, call_authenticated_api, my_new_tool, # ← 新增 ]

项目结构

app/ ├── main.py # FastAPI 入口 + 生命周期管理 ├── config.py # 环境变量配置 ├── database.py # 异步 SQLAlchemy 数据库连接 ├── models.py # 数据模型(User / ChatSession / ChatMessage) ├── auth.py # JWT 认证 + bcrypt 密码哈希 ├── engine.py # ★ LangChain 聊天引擎(工具调用循环 + 流式输出) ├── tools.py # ★ 工具定义(鉴权透传的核心实现) ├── routes_auth.py # 认证 API(注册 / 登录 / 登出) └── routes_chat.py # 聊天 API(会话管理 / 消息发送 / SSE 流式) templates/ ├── login.html # 登录 / 注册页面 └── index.html # 聊天页面 mock_server.py # Mock 外部服务(用于测试工具调用 + 鉴权透传)

快速开始

# 1. 安装依赖 pip install -r requirements.txt # 2. 配置环境变量 cp .env.example .env # 编辑 .env,填入 OPENAI_API_KEY 等配置 # 3. 启动 Mock 外部服务(可选,用于测试工具调用) python3 mock_server.py # 4. 启动主服务 python3 run.py # 5. 访问 http://localhost:8000

踩坑记录

问题原因解决方案
工具函数收不到 configconfig: Optional[RunnableConfig] = None 类型不对改为 config: RunnableConfig(keyword-only 参数)
passlib + bcrypt 5.x 报错passlib 与新版 bcrypt 不兼容弃用 passlib,直接使用 bcrypt 原生 API
工具调用场景下响应慢chat_streamainvokeastream,多调了一次 LLM全程只用 astream,通过收集 tool_call_chunks 判断

About

这是一个演示项目,展示如何使用 **LangChain** 实现 LLM 工具调用(Tool Calling),以及如何在工具执行时传递用户鉴权信息等额外参数。

Language
Python52.9%
HTML31.8%
CSS15.4%