以下内容已按 Markdown 格式重新整理并输出:


根据 Runnable 对象创建工具 —— 完整案例(基于千问 Qwen)

本案例演示如何将一个 LCEL 链(Runnable 对象)通过 as_tool() 方法封装成一个标准工具,并让 Agent 调用它。
核心知识点:as_tool() 的用法、Runnable 协议、工具 Schema 的自动生成。


1. 环境准备与模型初始化

# ========== 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 链)

# ========== 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 转换为工具

# ========== 3. 📌 使用 as_tool() 将Runnable转换为工具 ==========
translation_tool = translator_chain.as_tool(
    name="multi_lang_translator",
    description="""一个多语言翻译工具。接收源语言、目标语言和待翻译文本,
                   并将文本从源语言翻译成目标语言。""",
    # args_schema 可以手动指定,也可以由 LangChain 根据 Runnable 的输入自动推断
    args_schema=None
)

关键点

  • as_tool() 方法接受 namedescription 等参数,生成一个标准的 BaseTool 对象。
  • 工具的参数 Schema 会从 Runnable 的输入类型(通常是函数签名或 Pydantic 模型)自动生成。
  • 此处 translator_chain 是一个 RunnableSequence,其输入类型为 dict,包含 source_langtarget_langtext 三个字段。

4. 创建 Agent 并使用该工具

# ========== 4. 创建Agent并使用该工具 ==========
tools = [translation_tool]

agent = create_react_agent(
    llm,
    tools=tools,
    prompt="""你是一个智能助手,可以调用工具帮用户解决问题。
              当用户需要翻译时,请直接使用 multi_lang_translator 工具。"""
)

5. 测试运行

# ========== 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 链 使用 | 操作符串联多个 Runnable,形成一个处理管道
as_tool() 将任何 Runnable 对象转换为一个标准工具(BaseTool),使其能被 Agent 调用
工具 Schema 工具的输入参数结构,可从 Runnable 的输入类型自动推导,也可手动指定
Agent 调用工具 Agent 根据用户输入和工具描述,决定是否调用以及传递何种参数

8. 实践建议

  1. 修改参数:尝试改变 source_lang / target_lang 的默认值,观察 Agent 是否能正确理解。
  2. 添加新工具:再封装一个“文本摘要”Runnable,并添加到 tools 列表中。
  3. 深入理解:打印 translation_tool.args_schema,查看自动生成的 JSON Schema 结构。

如果需要查看某个 Runnable 的输入类型,可以使用:

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

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 调用(无任何框架)

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(封装了请求/响应细节)

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 模式)

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、本地模型等),切换模型只需改 modelbase_url
  • 消息格式转换(LangChain 内部使用 HumanMessageSystemMessage 等对象,自动序列化为 API 需要的格式)。
  • 内置工具调用(bind_tools)的支持。
  • 与 LCEL 链无缝集成。

5. 对话记忆的本质(多轮调用)

LLM 本身无状态,每次调用都是独立的。要实现多轮对话,需要手动维护消息列表,将历史对话每次都传进去。

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 的 invokestream 有什么区别?
A:invoke 一次性返回完整响应;stream 逐个 token 返回,适合实时显示打字效果。


通过这个例子,你应该理解了:无论框架多么复杂,底层永远是这个简单的 HTTP 请求-响应循环。LangChain、LangGraph 都是在之上构建更高级的抽象(链、图、工具调用等)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐