Skip to content

从零到进阶做 Agent(四)LangChain 入门

🕒 Published at:

到目前为止我们都在用裸 openai SDK。它够用,但当应用变复杂——要管模板、要解析输出、要接检索、要串多个步骤——手写会越来越乱。LangChain 就是把这些环节标准化的框架。这一篇讲它最核心的三件套:模型接入PromptTemplate(提示词模板)LCEL(把步骤串成链),以及 OutputParser(输出解析)。顶部可切换 Python / Node.js。

本系列基于 LangChain 1.x(Python)/ LangChain.js 1.0(Node)。这是目前的最新大版本,接口和网上很多旧教程不一样,请以本文为准。

装依赖

我们继续用通义千问的 OpenAI 兼容接口,所以模型走 langchain-openai / @langchain/openai 这个适配包。

bash
# -U 是 --upgrade 的简写:把包装成最新版本(已经装过的会升级到最新)
pip install -U langchain langchain-openai
bash
npm install @langchain/core @langchain/openai

一、接入模型

LangChain 把「聊天模型」抽象成一个统一对象。因为通义千问兼容 OpenAI,我们用 ChatOpenAI,把地址指到百炼即可。注意两边传 base_url 的写法略有不同:Python 直接传 base_url,Node 放在 configuration.baseURL 里。

python
import os
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="qwen-plus",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    temperature=0,
)

# 最简单的一次调用:传入一段文本,拿到一个消息对象
resp = model.invoke("用一句话介绍 LangChain。")
print(resp.content)     # .content 才是文本
javascript
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  model: "qwen-plus",
  apiKey: process.env.DASHSCOPE_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  },
});

// 最简单的一次调用:传入一段文本,拿到一个消息对象
const resp = await model.invoke("用一句话介绍 LangChain。");
console.log(resp.content);   // .content 才是文本

注意 invoke 返回的是一个消息对象(AIMessage),文本在 .content 里,而不是像裸 SDK 那样深挖 choices[0].message.content

二、PromptTemplate:把提示词做成模板

真实应用里,提示词往往是「固定框架 + 变量」。PromptTemplate 让你用 {变量} 占位,调用时再填值,避免手动拼字符串。

python
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个{role},回答尽量简洁。"),
    ("human", "请解释:{topic}"),
])

# 填入变量,得到最终的一组消息
messages = prompt.invoke({"role": "Python 老师", "topic": "什么是装饰器"})
print(messages)
javascript
import { ChatPromptTemplate } from "@langchain/core/prompts";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "你是一个{role},回答尽量简洁。"],
  ["human", "请解释:{topic}"],
]);

// 填入变量,得到最终的一组消息
const messages = await prompt.invoke({ role: "JS 老师", topic: "什么是闭包" });
console.log(messages);

每条消息前面的 system / human 就是角色,和第一篇讲的 messages 角色是同一回事,只是 LangChain 换了两个叫法

  • system:系统设定,定人设和规则(和裸 SDK 完全一样)。
  • human:用户说的话,等同于裸 SDK 里的 user
  • ai:模型的回复,等同于裸 SDK 里的 assistant(多轮对话时才用到)。

也就是说,human = userai = assistant,含义没变,只是 LangChain 习惯用「human / ai」这套更口语化的命名。上面模板里 {role}{topic} 这种花括号则是变量占位符invoke 时用同名字段填进去——注意别把「消息角色」和「变量」搞混,前者是每条消息的身份,后者是模板里待填的空。

三、LCEL:用管道把步骤串起来

这是 LangChain 的精髓。LCEL(LangChain Expression Language)让你像搭水管一样,把「模板 → 模型 → 解析器」串成一条:数据从左流到右。Python 用 | 运算符,Node 用 .pipe()

这个 | 背后的原理——为什么组件能随意拼接、为什么串完就自带流式和批处理——我单独写了一篇深入讲解,想搞懂机制的话强烈推荐先看它:

StrOutputParser / StringOutputParser 的作用是把模型返回的消息对象自动«拆»成纯文本字符串,省得每次自己取 .content

python
from langchain_core.output_parsers import StrOutputParser

chain = prompt | model | StrOutputParser()
#        模板   →  模型  →  取出纯文本

text = chain.invoke({"role": "Python 老师", "topic": "什么是装饰器"})
print(text)     # 直接就是字符串
javascript
import { StringOutputParser } from "@langchain/core/output_parsers";

const chain = prompt.pipe(model).pipe(new StringOutputParser());
//            模板       →  模型   →       取出纯文本

const text = await chain.invoke({ role: "JS 老师", topic: "什么是闭包" });
console.log(text);   // 直接就是字符串

一旦串成链,它就自带 invoke(要结果)、stream(流式)、batch(批量)等能力。比如流式输出,只需把 invoke 换成 stream

python
for chunk in chain.stream({"role": "Python 老师", "topic": "什么是装饰器"}):
    print(chunk, end="", flush=True)   # 链已解析,chunk 直接是文本片段
print()
javascript
const stream = await chain.stream({ role: "JS 老师", topic: "什么是闭包" });
for await (const chunk of stream) {
  process.stdout.write(chunk);   // 链已解析,chunk 直接是文本片段
}
process.stdout.write("\n");

对比第二篇里手写流式的代码,这里干净了很多——因为解析、拼接这些活儿链帮你干了。

四、JsonOutputParser:结构化输出更省心

上一篇我们手动 json.loads。LangChain 提供 JsonOutputParser,串进链里就能自动把模型输出解析成对象。

python
from langchain_core.output_parsers import JsonOutputParser

json_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是信息抽取助手,只输出 json:{{\"city\": ..., \"temp\": ...}}"),
    ("human", "{text}"),
])
json_chain = json_prompt | model | JsonOutputParser()

data = json_chain.invoke({"text": "今天杭州 28 度。"})
print(data["city"], data["temp"])   # 已经是 dict
javascript
import { JsonOutputParser } from "@langchain/core/output_parsers";

const jsonPrompt = ChatPromptTemplate.fromMessages([
  ["system", '你是信息抽取助手,只输出 json:{{"city": ..., "temp": ...}}'],
  ["human", "{text}"],
]);
const jsonChain = jsonPrompt.pipe(model).pipe(new JsonOutputParser());

const data = await jsonChain.invoke({ text: "今天杭州 28 度。" });
console.log(data.city, data.temp);   // 已经是对象

小坑:模板里如果要输出字面量的花括号(比如给模型看 JSON 例子),要写成双花括号 ,否则会被当成变量占位符。

小结与预告

这一篇你掌握了 LangChain 的骨架:模型(ChatOpenAI)→ 模板(PromptTemplate)→ 链(LCEL 的 | / .pipe())→ 解析(OutputParser)。以后无论多复杂的应用,本质都是往这条链上加环节。

但现在这条链还是「一问一答、转身就忘」。下一篇《会话记忆》,我们给它接上记忆,让它能记住多轮对话——并且用比第二篇「手动维护 messages 列表」优雅得多的方式实现。