以下内容已按 Markdown 格式重新整理并输出:
根据 Runnable 对象创建工具 ------ 完整案例(基于千问 Qwen)
本案例演示如何将一个 LCEL 链 (Runnable 对象)通过 as_tool() 方法封装成一个标准工具,并让 Agent 调用它。
核心知识点:as_tool() 的用法、Runnable 协议、工具 Schema 的自动生成。
1. 环境准备与模型初始化
python
# ========== 1. 环境准备与模型初始化 ==========
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate
from langgraph.prebuilt import create_react_agent
load_dotenv()
# 1. 初始化模型(确保 .env 中配置了 DASHSCOPE_API_KEY 和 DASHSCOPE_BASE_URL)
llm = ChatOpenAI(
model="qwen-plus",
temperature=0.8,
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL"),
model_kwargs={"extra_body": {"enable_thinking": False}},
)
2. 定义核心翻译逻辑为 Runnable(LCEL 链)
python
# ========== 2. 定义核心翻译逻辑为Runnable ==========
def build_translator_chain():
"""构建翻译链"""
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个专业的翻译,能将 {source_lang} 翻译成 {target_lang}。只返回翻译结果,不做其他解释。"),
("human", "{text}")
])
# 📌 构建LCEL链:提示词 → 模型
chain = prompt | llm
return chain
# 创建链
translator_chain = build_translator_chain()
3. 使用 as_tool() 将 Runnable 转换为工具
python
# ========== 3. 📌 使用 as_tool() 将Runnable转换为工具 ==========
translation_tool = translator_chain.as_tool(
name="multi_lang_translator",
description="""一个多语言翻译工具。接收源语言、目标语言和待翻译文本,
并将文本从源语言翻译成目标语言。""",
# args_schema 可以手动指定,也可以由 LangChain 根据 Runnable 的输入自动推断
args_schema=None
)
关键点:
as_tool()方法接受name、description等参数,生成一个标准的BaseTool对象。- 工具的参数 Schema 会从 Runnable 的输入类型(通常是函数签名或 Pydantic 模型)自动生成。
- 此处
translator_chain是一个RunnableSequence,其输入类型为dict,包含source_lang、target_lang、text三个字段。
4. 创建 Agent 并使用该工具
python
# ========== 4. 创建Agent并使用该工具 ==========
tools = [translation_tool]
agent = create_react_agent(
llm,
tools=tools,
prompt="""你是一个智能助手,可以调用工具帮用户解决问题。
当用户需要翻译时,请直接使用 multi_lang_translator 工具。"""
)
5. 测试运行
python
# ========== 5. 测试Agent ==========
if __name__ == "__main__":
user_input = "我想把这段英文 'LangGraph is great!' 翻译成中文,可以吗?"
print(f"用户:{user_input}\n")
final_state = None
for event in agent.stream(
{"messages": [HumanMessage(content=user_input)]},
stream_mode="values"
):
if "messages" in event:
last_msg = event["messages"][-1]
last_msg.pretty_print()
final_state = event
if final_state and "messages" in final_state:
final_answer = final_state["messages"][-1].content
print(f"\n最终答案:{final_answer}")
6. 预期输出示例
用户:我想把这段英文 'LangGraph is great!' 翻译成中文,可以吗?
================================ Human Message =================================
我想把这段英文 'LangGraph is great!' 翻译成中文,可以吗?
================================== Ai Message ==================================
Tool Calls:
multi_lang_translator (call_xxx)
Call ID: call_xxx
Args:
source_lang: 英语
target_lang: 中文
text: LangGraph is great!
================================= Tool Message =================================
Name: multi_lang_translator
LangGraph 太棒了!
================================== Ai Message ==================================
已经为您将"LangGraph is great!"翻译成中文:"LangGraph 太棒了!"
最终答案:已经为您将"LangGraph is great!"翻译成中文:"LangGraph 太棒了!"
7. 关键知识点总结
| 概念 | 说明 |
|---|---|
| Runnable | LangChain 中所有可执行组件的抽象基类(如 Prompt、Model、LCEL 链) |
| LCEL 链 | 使用 ` |
as_tool() |
将任何 Runnable 对象转换为一个标准工具(BaseTool),使其能被 Agent 调用 |
| 工具 Schema | 工具的输入参数结构,可从 Runnable 的输入类型自动推导,也可手动指定 |
| Agent 调用工具 | Agent 根据用户输入和工具描述,决定是否调用以及传递何种参数 |
8. 实践建议
- 修改参数 :尝试改变
source_lang/target_lang的默认值,观察 Agent 是否能正确理解。 - 添加新工具 :再封装一个"文本摘要"Runnable,并添加到
tools列表中。 - 深入理解 :打印
translation_tool.args_schema,查看自动生成的 JSON Schema 结构。
如果需要查看某个 Runnable 的输入类型,可以使用:
python
print(translator_chain.input_schema.schema())
9.这个链式的写法是不是之前langchain中的,现在怎么在langgraph中也会提到?
LCEL 链式写法确实是 LangChain 的核心特性,而不是 LangGraph 独有的。但两者之间有着紧密的集成关系,所以你在 LangGraph 的资料中会频繁看到 LCEL 的身影。
你观察得很敏锐------LCEL 链式写法确实是 LangChain 的核心特性 ,而不是 LangGraph 独有的。但两者之间有着紧密的集成关系,所以你在 LangGraph 的资料中会频繁看到 LCEL 的身影。
下面详细解释它们的关系以及为什么 LangGraph 中会提到链式写法。
1. LangChain 与 LangGraph 的关系
| 框架 | 定位 | 核心能力 |
|---|---|---|
| LangChain | 通用 LLM 应用开发框架 | 提供 LCEL、Prompt 模板、输出解析器、文档加载器、向量存储、工具定义等 |
| LangGraph | 基于图结构的有状态 Agent 框架 | 专注于多步骤、可控制、带记忆和循环的复杂工作流(如 ReAct Agent、多 Agent 协作) |
LangGraph 构建在 LangChain 之上,复用了 LangChain 的核心组件:
- 所有 LangChain 的
Runnable对象(包括 LCEL 链)都可以作为 LangGraph 中的节点(Node)。 - LangChain 的工具系统(
BaseTool)可以直接被 LangGraph 的ToolNode使用。 - LangChain 的消息类型(
HumanMessage,AIMessage等)也是 LangGraph 状态的默认格式。
因此,LCEL 并不是被"移植"到 LangGraph,而是 LangGraph 天然继承了 LangChain 的这一表达方式。
2. 为什么在 LangGraph 中会使用 LCEL 链式写法?
在 LangGraph 中,一个图(Graph)的节点通常是一个可调用的函数或一个 Runnable。当节点的逻辑较为复杂(例如包含提示词模板 + LLM 调用 + 输出解析)时,使用 LCEL 可以非常简洁地定义这个节点。
示例:在 LangGraph 节点中使用 LCEL
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from typing import TypedDict
llm = ChatOpenAI(model="gpt-3.5-turbo")
# 定义一个 LCEL 链(Runnable)
summarize_chain = (
ChatPromptTemplate.from_template("请总结以下内容:\n{content}")
| llm
)
class State(TypedDict):
content: str
summary: str
def summarize_node(state: State):
# 调用 LCEL 链
result = summarize_chain.invoke({"content": state["content"]})
return {"summary": result.content}
builder = StateGraph(State)
builder.add_node("summarize", summarize_node)
builder.set_entry_point("summarize")
builder.add_edge("summarize", END)
app = builder.compile()
好处:
- 代码简洁,无需手动处理提示词拼接和模型调用细节。
- 可维护性强,LCEL 链可以独立测试和复用。
- 天然支持流式、批处理和异步操作。
3. LangGraph 的核心贡献不是 LCEL,而是图结构与状态管理
LangGraph 之所以存在,是因为 LCEL(或普通的顺序链)难以优雅地处理需要循环、条件分支、多步骤工具调用、跨步骤持久化状态的场景。LangGraph 提供了:
- 显式的节点和边
- 条件路由(
add_conditional_edges) - 共享状态对象(
State) - 持久化和检查点(Checkpointer)
而 LCEL 在其中扮演的是节点内部实现的角色,而不是图编排的角色。
4. 总结:你看到链式写法是正常的
| 场景 | 使用的技术 |
|---|---|
| 定义单个节点内部逻辑(如调用 LLM、处理输入) | LCEL(LangChain) |
| 将多个节点组合成带分支和循环的工作流 | LangGraph 的图 API |
两者互补而不是替代。LangGraph 不排斥 LCEL,反而鼓励在节点内部使用 LCEL 来保持代码简洁。
所以,你在 LangGraph 教程中看到 LCEL 链式写法,完全符合预期,并且是官方推荐的最佳实践------它体现了 LangChain 生态的统一性和模块化设计。
可以用一个例子让我明白llm 调用的核心逻辑吗?
LLM 调用的核心逻辑 ------ 从裸 API 到框架封装
为了让你彻底理解 LLM 调用的本质,我们用 最原始的方式 (requests 库)调用一次阿里云百炼的 Qwen 模型,然后逐步解释核心逻辑,再对比 LangChain 的封装做了什么。
1. 最原始的 LLM 调用(无任何框架)
python
import requests
import json
# 配置
api_key = "sk-你的阿里云百炼API Key"
url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
# 构造请求体(messages 是核心)
payload = {
"model": "qwen-plus",
"messages": [
{"role": "system", "content": "你是一个有用的助手。"},
{"role": "user", "content": "1+1等于几?"}
],
"temperature": 0.7,
"max_tokens": 100
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
# 发送请求
response = requests.post(url, headers=headers, json=payload)
# 解析响应
if response.status_code == 200:
result = response.json()
answer = result["choices"][0]["message"]["content"]
print(f"模型回答:{answer}")
else:
print(f"错误:{response.text}")
输出示例:
模型回答:1+1等于2。
2. 核心逻辑拆解(这是面试可能会问的)
| 步骤 | 说明 | 对应代码 |
|---|---|---|
| 1. 构造请求 | 组织好模型参数(model)、对话历史(messages)、生成参数(temperature、max_tokens) | payload = {...} |
| 2. 设置认证 | 大多数 API 使用 API Key 通过 Header 认证 | headers["Authorization"] = f"Bearer {api_key}" |
| 3. 发送 HTTP 请求 | POST 请求到指定的 endpoint | requests.post(url, ...) |
| 4. 解析响应 | 从返回的 JSON 中提取 choices[0].message.content |
result["choices"][0]["message"]["content"] |
| 5. 错误处理 | 处理网络错误、限流、认证失败等 | if response.status_code != 200 |
核心本质:
LLM 调用就是一个 HTTP 请求,输入是
messages(对话历史),输出是message.content(模型的回复)。所有框架(LangChain、OpenAI SDK 等)都是对这个过程的封装,让你不用手写 HTTP 细节。
3. 使用 OpenAI SDK(封装了请求/响应细节)
python
from openai import OpenAI
client = OpenAI(
api_key="sk-你的API Key",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
response = client.chat.completions.create(
model="qwen-plus",
messages=[
{"role": "system", "content": "你是一个有用的助手。"},
{"role": "user", "content": "1+1等于几?"}
],
temperature=0.7,
max_tokens=100
)
print(response.choices[0].message.content)
相比裸请求的改进:
- 自动处理 HTTP 细节(序列化、反序列化、重试、超时)
- 强类型响应对象(
response.choices[0].message.content) - 支持流式(
stream=True)
4. 使用 LangChain(进一步封装成 invoke 模式)
python
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="qwen-plus",
api_key="sk-你的API Key",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
temperature=0.7
)
messages = [
("system", "你是一个有用的助手。"),
("human", "1+1等于几?")
]
response = llm.invoke(messages)
print(response.content)
LangChain 做的额外事情:
- 统一了不同 LLM 提供商的接口(OpenAI、Anthropic、本地模型等),切换模型只需改
model和base_url。 - 消息格式转换(LangChain 内部使用
HumanMessage、SystemMessage等对象,自动序列化为 API 需要的格式)。 - 内置工具调用(
bind_tools)的支持。 - 与 LCEL 链无缝集成。
5. 对话记忆的本质(多轮调用)
LLM 本身无状态 ,每次调用都是独立的。要实现多轮对话,需要手动维护消息列表,将历史对话每次都传进去。
python
messages = [
{"role": "system", "content": "你是一个有用的助手。"}
]
while True:
user_input = input("用户:")
if user_input.lower() == "quit":
break
messages.append({"role": "user", "content": user_input})
# 调用 LLM
payload["messages"] = messages
response = requests.post(url, headers=headers, json=payload)
assistant_reply = response.json()["choices"][0]["message"]["content"]
messages.append({"role": "assistant", "content": assistant_reply})
print(f"助手:{assistant_reply}")
这就是所有对话 Agent 最基础的实现原理。
6. 核心逻辑总结图
┌─────────────────────────────────────────────────────────────┐
│ LLM 调用核心逻辑 │
├─────────────────────────────────────────────────────────────┤
│ 输入:messages (对话历史) + 参数 (model, temperature...) │
│ ↓ │
│ ① 构造 HTTP 请求(JSON 格式) │
│ ↓ │
│ ② 发送到 API 端点(带认证 Header) │
│ ↓ │
│ ③ 接收 JSON 响应 │
│ ↓ │
│ ④ 解析 response.choices[0].message.content │
│ ↓ │
│ 输出:模型的回复字符串 │
└─────────────────────────────────────────────────────────────┘
7. 面试可能追问的问题
Q:LLM 调用的 token 消耗如何计算?
A:每次请求的输入(messages)和输出(completion)都消耗 token。中文字符通常 1 个 token ≈ 1.5~2 个汉字,不同模型 tokenizer 不同。可以通过响应头或返回的 usage 字段查看。
Q:temperature 和 top_p 的作用?
A:控制随机性。temperature 越低输出越确定(趋向于概率最高的 token),越高越有创造性。top_p 控制采样范围(累积概率阈值)。两者通常不同时调整。
Q:如何处理 API 限流或超时?
A:实现重试机制(指数退避),使用异步请求,或本地缓存重复查询。
Q:LangChain 的 invoke 和 stream 有什么区别?
A:invoke 一次性返回完整响应;stream 逐个 token 返回,适合实时显示打字效果。
通过这个例子,你应该理解了:无论框架多么复杂,底层永远是这个简单的 HTTP 请求-响应循环。LangChain、LangGraph 都是在之上构建更高级的抽象(链、图、工具调用等)。