前言
注,langchain 更新速度很快,本文所有案例都经过试验,在 2025年12月可用!并且所有案例的 python 和 node.js 版本我都测试通过。
这是「写给小白学 AI」系列的第 5 篇文章,这个系列专门为刚接触人工智能的前端开发者和编程新手打造。如果你错过了之前的精彩内容,可以在这里补课:
- 打包票!前端和小白一定能明白的人工智能基础概念
- 不易懂你打我!写给前端和小白的 大模型(ChatGPT) 工作基本原理!
- 前端角度学 AI - 15 分钟入门 Python
- Prompt 还能哄女朋友!你真的知道如何问 ai 问题吗?
- 神兵在手,AI Agent 起步不愁:写给小白的 LangChain 入门指南
同时也欢迎加入国内目前最好的 react headless 组件库项目
学习提示:为了让不同技术背景的同学都能跟上节奏,本文的代码示例会同时提供 Python 和 Node.js 版本,你可以选择自己熟悉的语言来实践。
langchain 跟 coze(扣子) 这种可视化制作 ai agent 的平台不同,langchain 是用代码制作 ai agent,coze 是相当于低代码平台制作 ai agent。
而学习 langchain 的核心就是学习它各种模块如何使用,最终会用这些模块做一些 ai agent 案例。所以接下来我们看看 langchain 核心模块有哪些。
langchain 核心模块
接下来讲 langchain 如何制作 ai agent 的本质,就是介绍如何调用 langchain 核心模块的 api。其中它的核心模块包括:
- Model
- Chains
- Memory
- Agents
- Retrieval
- Calllbacks
其实核心概念不止这些,可以查看官网,如下图,左侧是比较重要的概念,我们的系列文章主要是想在最短时间,走通changchain部分核心模块,其它模块会穿插在其中讲。

我们刚开始肯定不理解模块的具体作用,这个千万别在意,我们接下来关于 langchain 的文章都是解释这几个模块的用法。我们先从一个简单的 hello world 案例了解一下,如何在 langchain 中调用大模型,然后开始进入 Model 模块。
hello world 案例的 python 代码如下(后面会有代码解释,先看案例):
注意,没有 python 环境的,请查阅网上资料安装对应 python 代码运行环境。
首先安装必要的依赖
python
pip install -U langchain langchain-openai python-dotenv
然后示例代码如下
python
import dotenv
from langchain_openai import ChatOpenAI
import os
dotenv.load_dotenv()
llm = ChatOpenAI(
model_name=os.getenv("MODEL_NAME"),
temperature=0,
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
response = llm.invoke("你好!")
print(response)
node.js 代码:
安装依赖:
bash
npm install dotenv langchain @langchain/core @langchain/openai
创建 test.mjs 文件(.mjs 结尾的目的是用 es6 module 加载模块)
接着看 demo 代码:
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
dotenv.config(); // 加载 .env 文件
// 初始化 LLM
const llm = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
temperature: 0,
openAIApiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL,
});
async function main() {
const response = await llm.invoke("你好!");
console.log(response);
}
main();
首先解释一下这些库:
- dotenv 是用来加载环境变量的,外部会有一个.env 文件,可以书写环境变量,例如 我的 .env 文件如下,(其中 OPENAI_API_KEY 你要换成你自己的,这个是需要付费的,我购买的是 deepseek 的api, 充值 10 元就可以用很久,练习足够了):
ini
OPENAI_API_KEY="sk-xxxx"
OPENAI_BASE_URL="https://api.deepseek.com"
MODEL_NAME="deepseek-chat"
例如上面定义了 OPENAI_BASE_URL 环境变量,在 python 文件中,我就可以通过
python
# 加载 .env 中配置的环境变量
dotenv.load_dotenv()
# 通过 os 模块得到刚才加载的的环境变量的值
os.getenv("OPENAI_BASE_URL")
在 node.js中是
javascript
dotenv.config(); // 加载 .env 文件
# 通过 process.env 模块得到刚才加载的的环境变量的值
process.env.OPENAI_BASE_URL
- ChatOpenAI 就是调用 chatgpt api 的 langchain 的包,因为 deepseek 大体兼容 OpenAI 的 api (deepseek 也有自己独有的包,我们只是练习,可以忽略这个问题)
Model 模块
接下来我们进入到第一个 Model 模块的讲解。
Model 模块的作用是什么?
Model 模块的作用本质是抽象与统一对各类大模型(LLM、Chat Model、Embedding Model 等)的调用接口。简单来说就是不同的大模型有不同的 api,在 langchain 这里把调用它们的方式统一在一起。当然也有细微差别,例如 openai 有 openai 对应的 model 的包,Claude 有 Claude 对应的 model 包。但使用方式几乎都是一样的 。
并且返回的格式几乎也是一样的。这就是 Model 最基本的作用。
刚才在我们的 hello world 代码中,其实我们已经用了 Model 模块了。代码如下:
llm.invoke
这个 invoke 就是 Model 模块其中一个核心的方法,简单来说就是一问一答, invoke 函数调用一次, 大模型回答一次。当然 Model 的功能远不止如此。
例如我们看到很多大模型的官网,我们问问题的时候,都是一个字一个字吐出来的,这种流式数据,在 Model 中,需要调用 stream 方法。我们后续介绍。
好了,简单理解 Model 就是用来调大模型 api 以及接受大模型返回数据的模块即可!
invoke 示例
我们刚才已经介绍了,这里再次复习一下,这里注意,我们可以链式调用:
python 案例
python
import dotenv
from langchain_openai import ChatOpenAI
import os
dotenv.load_dotenv()
llm = ChatOpenAI(
model_name=os.getenv("MODEL_NAME"),
temperature=0,
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
response = llm.invoke("你好!")
print(response)
node.js 案例
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
dotenv.config(); // 加载 .env 文件
// 初始化 LLM
const llm = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
temperature: 0,
openAIApiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL,
});
async function main() {
const response = await llm.invoke("你好!");
console.log(response);
}
main();
需要注意,其中 ChatOpenAI 就是 Model 中用来调用 Chatgpt 的包。 invoke 方法的特性就是一次性使用,没有上下轮记忆功能,也就是第一次调 invoke 和 第二次调 invoke 没什么关系。(除非你把第一次 invoke 的内容作为第二次的历史发给大模型)。
我们上面定义的 invoke 的参数是字符串,它还支持一些别的类型。接下来我们一一介绍:
消息类型
除了字符串,langchain 还提供了一些消息类型,例如:
-
SystemMessage: 设定整体行为规范、角色定位、目标约束。例如:
- 你是一个资深翻译专家
- 输出 JSON 格式
-
HumanMessage:代表用户的自然语言输入,是用户真实提问或要求。也就是我们的问题。例如:
- 1 + 1 等于几?
- 鸟会飞吗?
-
AIMessage: 表示模型的先前回复,用于保留上下文。我们知道 invoke 没有记忆功能(大模型本身没有记忆功能)。我们如果想让其有记忆,就需要把之前的问题和结果在 invoke 函数中插入,而插入的类型,我们可以使用 AIMessage 来做。
-
ToolMessage 或者叫 FunctionMessage,模型调用工具(API Function)的结构化结果,例如:
- 模型调用"搜索天气"后,我们的工具调用网络 api 去查询到底天气如何,假设返回:"北京 23 度,多云",该结果被包装成 ToolMessage,再输入给模型推理下一步逻辑。
- 这个方式就是很多文章说到的 ai agent 的概念,叫 MCP。
我们举个例子:
python例子如下:
python
import dotenv
from langchain_openai import ChatOpenAI
from langchain.messages import HumanMessage, SystemMessage
import os
dotenv.load_dotenv()
llm = ChatOpenAI(
model_name=os.getenv("MODEL_NAME"),
temperature=0,
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
system_message = SystemMessage(content="你是一个数学方面的专家")
human_message = HumanMessage(content="1+1等于多少?")
messages = [system_message, human_message]
response = llm.invoke(messages)
print(response)
node.js例子如下:
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "langchain";
dotenv.config(); // 加载 .env 文件
// 初始化 LLM
const llm = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
temperature: 0,
openAIApiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL,
});
const messages = [
new SystemMessage("你是一个数学方面的专家"),
new HumanMessage("1+1等于多少?"),
];
async function main() {
const response = await llm.invoke(messages);
console.log(response);
}
main();
可以看到,说白了,之前提问,我们直接用字符串,现在只不过用 SystemMessage 和 HumanMessage 实例化我们的问题而已,然后把问题区分了类型而已
- SystemMessage:全局性的关键信息,或者说背景信息
- HumanMessage:用户提问
流式响应
之前调用 invoke 方法,我们会等一会儿,接收到数据是一次性,例如我之前调用deepseek返回格式如下:
swift
{
"id": "953a9be0-f578-4477-9c63-b1ffcda1e3ee",
"content": "这是一个非常基础的数学问题。 \n\n根据算术的基本定义: \n\\[\n1 + 1 = 2\n\\] \n\n如果你是在二进制系统中,那么: \n\\[\n1_2 + 1_2 = 10_2\n\\] \n(即十进制中的 2) \n\n如果你是在布尔代数中,有时 \\(1 + 1 = 1\\)(逻辑或运算)。 \n\n不过,通常默认在十进制算术下,答案是 **2**。",
"additional_kwargs": {},
xxx省略其它参数。
}
以上是 node.js 的返回结果,是 json 格式,然后,我们可以看到 content 是一次性返回的。
有时候我们希望自己的 ai agent 返回数据能够一个字一个字的往外吐,跟很多大模型的官网一致。在langchain 中需要调用 stream 方法,我们上案例:
python例子如下:
python
import dotenv
from langchain_openai import ChatOpenAI
from langchain.messages import HumanMessage, SystemMessage
import os
dotenv.load_dotenv()
llm = ChatOpenAI(
model_name=os.getenv("MODEL_NAME"),
temperature=0,
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
system_message = SystemMessage(content="你是一个数学方面的专家")
human_message = HumanMessage(content="1+1等于多少?")
for chunk in llm.stream([system_message, human_message]):
print(chunk.content, end="", flush=True)
print("\n---done---")
注意,在 python 中,需要使用 for xx in 语法来读取数据流。
node.js例子如下:
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "langchain";
dotenv.config(); // 加载 .env 文件
// 初始化 LLM
const llm = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
temperature: 0,
openAIApiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL,
});
const messages = [
new SystemMessage("你是一个数学方面的专家"),
new HumanMessage("1+1等于多少?"),
];
async function main() {
const stream = await llm.stream(messages);
for await (const chunk of stream) {
process.stdout.write(chunk.text);
}
}
main();
这里 node.js 使用 for await ... of 语法来遍历返回的数据流。我们对这个语法简单介绍一下:
for await...of 用于遍历异步的 Iterator 接口, 我们简单举个例子来说明基本用法:
javascript
// 模拟异步数据源
async function* getAsyncData() {
const data = [1, 2, 3, 4, 5];
for (const item of data) {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 100));
}
}
async function processData() {
console.log('开始处理...');
for await (const value of getAsyncData()) {
console.log(`处理结果: ${value}`);
// 1秒后输出: 处理结果: 1
// 2秒后输出: 处理结果: 2
// ...
}
console.log('处理完成!');
}
processData();
其中注意我们打印数据使用的是:
arduino
process.stdout.write(chunk.text);
而不是 console.log 的原因是,console.log 会让每次后端返回的数据后面加个换行符,为了不让其自动加换行符,我们用了 process.stdout.write api,相当于不加换行符的 console.log。
批量调用
除了 invoke, stream 方法,langchain 还有提供一个 batch 方法,这个方法主要是用来批量调用的,简单来说就是同时发多个消息。我们举个例子:
python例子如下:
python
import dotenv
from langchain_openai import ChatOpenAI
from langchain.messages import HumanMessage, SystemMessage
import os
dotenv.load_dotenv()
llm = ChatOpenAI(
model_name=os.getenv("MODEL_NAME"),
temperature=0,
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
system_message = SystemMessage(content="你是一个数学方面的专家")
human_message = HumanMessage(content="1+1等于多少?")
message1 = [system_message, human_message]
system_message1 = SystemMessage(content="你是一个语文方面的专家")
human_message1 = HumanMessage(content="请问"草长莺飞"出自哪首诗?")
message2 = [system_message1, human_message1]
response = llm.batch([message1, message2])
print(response)
返回结果一个列表,我们上面发了两个信息,所以列表长度就是 2,相当于以前 invoke 是一个一个发消息,batch 是批量一起发。
node.js例子如下:
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "langchain";
dotenv.config(); // 加载 .env 文件
// 初始化 LLM
const llm = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
temperature: 0,
openAIApiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL,
});
const messages1 = [
new SystemMessage("你是一个数学方面的专家"),
new HumanMessage("1+1等于多少?"),
];
const messages2 = [
new SystemMessage("你是一个语文方面的专家"),
new HumanMessage("请问"草长莺飞"出自哪首诗?"),
];
async function main() {
const response = await llm.batch([messages1, messages2]);
console.log("response: ", response);
}
main();
返回结果是一个数组,包含 messages1, messages2 分别返回的信息。相当于以前 invoke 是一个一个发消息,batch 是批量一起发。
ainvoke 和 astream
这两个 api 只在 python 版本中有。为什么呢?
invoke 是同步调用,ainvoke 是异步调用,异步就是不阻塞当前的代码,而 node.js 版本的 invoke 跟 python 默认是同步的调用完全不一样,node.js 中默认 invoke 是 promise,返回的也是 promise,所以默认就是异步的。所以不需要这两个 api。
接下来我们学习一下如何更复杂的方式跟 ai 提问。Model 组件提供了提示词模板功能。我们先看看什么是提示词模板。
提示词模板
我们先简单介绍一下 langchain 里的提示词模板是什么。
简单来说就是用于将动态变量嵌入固定文本模板,从而生成最终给模型的提示(prompt)。核心思想是"模板化+变量替换"。
举一个例子(以下是 node.js 案例):
-
首先定义模板字符串
- 定义固定文本和占位符
- 占位符通常用
{变量名}表示
示例:
css你是一个数学专家,请帮我计算: {expression} -
输入变量(input_variables)
- 模板中需要填充的变量
- LangChain 会要求用户在生成最终 prompt 时提供这些变量
iniconst template = "你是一个数学专家,请帮我计算: {expression}"; const variables = { expression: "1+1" };
以下是 node.js 案例
-
生成最终提示
- 使用模板 + 变量 → 最终 prompt 文本
- LangChain 会通过
PromptTemplate.format(variables)或类似方法生成
javascriptimport { PromptTemplate } from "langchain"; const prompt = new PromptTemplate({ // 字符串模板 template: "你是一个数学专家,请帮我计算: {expression}", // 意思是要填充到 template 的变量是 expression inputVariables: ["expression"] }); const finalPrompt = prompt.format({ expression: "1+1" }); console.log(finalPrompt); // 输出: 你是一个数学专家,请帮我计算: 1+1
其中生成模板的类型有好几种,如下(先列出来,后面一一介绍):
- PromptTemplate: 最基础的文本模板,支持将变量插入到固定文本中。
- FewShotPromptTemplate:用于 Few-Shot 学习场景,向模型提供示例问答,让模型理解期望格式,主要特点是可以往里面加 example,也就是案例。
- ChatPromptTemplate(聊天模板):构建多角色聊天场景的模板(系统消息、用户消息、助手消息),这个模板中还可以嵌套一些多种消息类型,包括:
SystemMessagePromptTemplate、HumanMessagePromptTemplate、AIMessagePromptTemplate
PromptTemplate
主要参数包括:
- template:定义提示词模板字符串,其中包含文本和变量占位符,例如 {name}
- input_variable: 指定了制定了模板中使用的变量名称,在调用模板的时候被替换
- partial_variable: 用于定义模板中一些固定的变量名,也就是提前替换模板中的占位符
先来个案例:
python 版本:
python
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate(
template="你是一个{role},你的名字是: {name}",
input_variables=["role", "name"],
partial_variables={"name": "张三"},
)
result = prompt.format(role="数学专家")
print("result", result) # 输出: 你是一个数学专家,你的名字是: 张三
node.js 版本:
javascript
import { PromptTemplate } from "@langchain/core/prompts";
const prompt = new PromptTemplate({
template: "你是一个{role},你的名字是: {name}",
inputVariables: ["role", "name"],
partialVariables: { name: "张三" },
});
async function runPrompt() {
const result = await prompt.format({ role: "数学专家" });
console.log("result", result); // 输出: 你是一个数学专家,你的名字是: 张三
}
runPrompt();
可以看到
- template 定义了一个模板,通过 {xxx} 来告诉你能插入什么变量在其中
- inputVariables,就是告诉能插入的变量名包括哪些
- partialVariables,直接插入默认的变量名
最终通过调用 format 方法,来给模板插入变量,最终完成字符串的拼接。
同时,你可以还可以链式调用:
javascript
import { PromptTemplate } from "@langchain/core/prompts";
async function runPrompt() {
const prompt = await new PromptTemplate({
template: "你是一个{role},你的名字是: {name}",
inputVariables: ["role", "name"],
}).partial({ name: "张三" });
// 注意上面的 partial 的链式调用,partial 方法是帮我们提前填充了数据
const result = await prompt.format({ role: "数学专家" });
console.log("result", result); // 输出: 你是一个数学专家,你的名字是: 张三
}
runPrompt();
如何利用这个模板传给大模型呢?
因为我们发现实质上 format 方法其实最终返回的只是一个字符串,字符串如何发给大模型呢?之前是不是介绍了 Model 模块有个 invoke 方法,是不是调用后就传给大模型了。这下就联系起来上下文的知识了。
多轮对话与上下文记忆
上面我们说了,之前 invoke 的调用是一次性的,第一次提问和第二次提问是没有关系的是什么意思呢,我们用伪代码简单举个例子(node.js 代码):
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "langchain";
dotenv.config(); // 加载 .env 文件
// 第一个问题是告诉大模型我叫小智
const message1 = [new SystemMessage("我叫小智")];
const message2 = [new HumanMessage("我叫什么?")];
const llm = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
temperature: 0,
openAIApiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL,
});
async function runPrompt() {
const response1 = await llm.invoke(message1);
const response2 = await llm.invoke(message2);
console.log("Response to message1:", response2.content);
}
runPrompt();
返回的结果如下(我问的deepseek):
我是DeepSeek,一个AI助手,由深度求索公司创造。我很乐意成为你的朋友和助手,无论你有什么问题、想法还是需要帮助的地方,都可以随时告诉我。
你看,完全不记得我们第一个问题了。如何让 ai 记住呢?
我们拿上面的代码改造一下
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage, AIMessage } from "langchain";
dotenv.config(); // 加载 .env 文件
// 第一个问题是告诉大模型我叫小智
const message1 = [new SystemMessage("我叫小智")];
const message2 = [new HumanMessage("我叫什么?")];
const llm = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
temperature: 0,
openAIApiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL,
});
async function runPrompt() {
const response1 = await llm.invoke(message1);
// AIMessage 就是 ai 回答的内容
const aiMessage = new AIMessage(response1.content);
// 将 AI 的回答也加入到上下文中
const response2 = await llm.invoke([...message1, aiMessage, ...message2]);
console.log("Response to message1:", response2.content);
}
runPrompt();
这下我们看看deepseek如何回答:
你刚才告诉我,你的名字是 小智 !😊
如果这不是你真实的名字,或者你想让我用其他名字称呼你,随时告诉我哦~
我会记住的!
这下就相当于有上下文记忆能力了。
有人就会好奇,你说大模型没记忆能力是不是搞错了,明明我们在用这些大模型官网的时候,都有呀。其实这些大模型的聊天类型的产品,算是 ai agent 应用了,不是简单的大模型调用。
ChatPromptTemplate
之前的 PromptTemplate 本质上生成的是一个 字符串,接下来我们看看更复杂的模板 ChatPromptTemplate。它是创建聊天消息列表的提示模板。它比普通的 PromptTemplate 更是适合处理多角色,多轮次对话。
- 它支持例如 System/Human/Ai 等不同角色的消息模板
- 历史对话维护
我们先来个简单的案例,看看如何使用:
先看 python 代码:
python
import dotenv
from langchain_openai import ChatOpenAI
from langchain.messages import HumanMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate
import os
prompt = ChatPromptTemplate(
messages=[
("system", "你是一个英语老师,你的名字是: {name}"),
("human", "我的问题是{question}"),
],
input_variables=["name", "question"],
)
res = prompt.invoke(
input={"name": "李四", "question": "What is the capital of France?"}
)
print("res", res)
返回内容是
ini
messages=[SystemMessage(content='你是一个英语老师,你的名字是: 李四', additional_kwargs={}, response_metadata={}), HumanMessage(content='我的问题是What is the capital of France?', additional_kwargs={}, response_metadata={})]
可以看到内容跟 PromptTemplate 结果大不一样,返回的是一个列表(python把数组叫列表),然后包含一个SystemMessage,包含一个 HumanMessage。
以下是 node.js 案例
typescript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "langchain";
import { ChatPromptTemplate } from "@langchain/core/prompts";
dotenv.config();
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个英语老师,你的名字是: {name}"],
["human", "我的问题是{question}"],
]);
async function printResult() {
const res = await prompt.invoke({
name: "李四",
question: "What is the capital of France?",
});
console.log("res: ", res);
}
printResult();
得到的结果如下:
javascript
ChatPromptValue {
// ... 省略无用信息
messages: [
SystemMessage {
"content": "你是一个英语老师,你的名字是: 李四",
"additional_kwargs": {},
"response_metadata": {}
},
HumanMessage {
"content": "我的问题是What is the capital of France?",
"additional_kwargs": {},
"response_metadata": {}
}
]
}
可以看到最终结果是一个 messages 数组然后包含一个 SystemMessage,包含一个 HumanMessage。注意返回的是 ChatPromptValue 类型,跟之前 PromptTemplate 的 format 方法返回的 str 是不一样的。
小小总结一下
- invoke 方法:传入字典(对象),返回 ChatPromptValue 类型
- format 方法:传入变量的值(对象),返回字符串类型。
其实还有两种调用方式,跟上面是类似的方式,我们这里做个简单的介绍,如何调用以及返回的类型
- python: format_message(), node.js formatMessage()
- python: format_prompt(), node.js fromPromt()
这个很简单,我们只拿 node.js 举例,注意下面的 prompt.formatMessages 方法:
javascript
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个英语老师,你的名字是: {name}"],
["human", "我的问题是{question}"],
]);
async function printResult() {
const res = await prompt.formatMessages({
name: "李四",
question: "What is the capital of France?",
});
console.log("res: ", res);
}
printResult();
返回的就是一个数组,内容为
css
[ SystemMessage { "content": "你是一个英语老师,你的名字是: 李四", "additional_kwargs": {}, "response_metadata": {} }, HumanMessage { "content": "我的问题是What is the capital of France?", "additional_kwargs": {}, "response_metadata": {} }]
注意下面的 rompt.formatPromptValue 方法:
javascript
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个英语老师,你的名字是: {name}"],
["human", "我的问题是{question}"],
]);
async function printResult() {
const res = await prompt.formatPromptValue({
name: "李四",
question: "What is the capital of France?",
});
console.log("res: ", res);
}
printResult(
返回:
css
ChatPromptValue {
// ...忽略其它属性
messages: [
SystemMessage {
"content": "你是一个英语老师,你的名字是: 李四",
"additional_kwargs": {},
"response_metadata": {}
},
HumanMessage {
"content": "我的问题是What is the capital of France?",
"additional_kwargs": {},
"response_metadata": {}
}
]
}
好像上面的 prompt.formatPromptValue 和 prompt.invoke 方法传参和返回值都差不多,但一般情况都会用 invoke。
invoke 返回值可以使用到后面我们学习到的 chain 相关的方法(模型链),而 prompt.formatPromptValue 仅仅是返回 ChatPromptValue 类型的数据而已。
我们再来一个好玩的,了解即可, 就是 toString 方法,可以将 ChatPromptValue 类型转化为字符串,如下,注意 res.toString():
javascript
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个英语老师,你的名字是: {name}"],
["human", "我的问题是{question}"],
]);
async function printResult() {
const res = await prompt.formatPromptValue({
name: "李四",
question: "What is the capital of France?",
});
console.log("res: ", res.toString());
}
printResult();
// 返回两行字符串,如下:
// System: 你是一个英语老师,你的名字是: 李四
// Human: 我的问题是What is the capital of France?
同理,还可以提取里面的 message,注意下面的 res.toChatMessages() 方法,能直接返回 message 消息数组:
javascript
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个英语老师,你的名字是: {name}"],
["human", "我的问题是{question}"],
]);
async function printResult() {
const res = await prompt.formatPromptValue({
name: "李四",
question: "What is the capital of France?",
});
console.log("res: ", res.toChatMessages());
}
最终上我们其实得到的就是一个提示词模板,然后将其传入大模型的 invoke 方法就可以问大模型问题了。
MessagePlaceholder
MessagePlaceholder 就是一个占位符,什么意思呢,一看案例马上明白:
python 案例如下:
python
import dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage
import os
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个知识渊博的助手,擅长回答各种各样的问题。"),
MessagesPlaceholder("history"),
]
)
res = prompt.invoke({"history": [HumanMessage(content="请问地球到月球的距离是多少?")]})
print("res", res)
是不是很简单,说白了,就是一个占位符,在prompt.invoke的时候传消息,而不是在 ChatPromptTemplate.from_messages 初始化的时候传消息。
node.js 代码如下:
javascript
import { HumanMessage } from "langchain";
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个英语老师,你的名字是: {name}"],
new MessagesPlaceholder({ variableName: "history" }),
]);
async function printResult() {
const res = await prompt.invoke({
name: "小2",
history: [new HumanMessage("What is the capital of France?")],
});
console.log("res: ", res);
}
printResult();
解释同上,也就是 MessagesPlaceholder 就是一个占位符,在 prompt.invoke 的时候传消息,而不是在 ChatPromptTemplate.from_messages 初始化的时候传消息。
FewShotPromptTemplate
我们知道,给大模型的问题当中,如果有一些案例,会极大的提高大模型回答的准确率,langchain 也提供了专门书写案例的提示词模板。
我们先介绍第一种最简单的跟提供案例相关的模板:FewShotPromptTemplate,FewShotPromptTemplate 必须要结合之前我们说的 PrompTemplate 一起使用。
使用方式有点绕,我们一步步的来解释,使用方式如下(使用方式 node.js 和python的都用python代码表示一下,后面具体案例会把两种代码都贴出来)
python
few_shot_prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=prompt,
suffix="input:{input} output:", # 声明在示例后面的提示词模板
input_variables=["input"],
)
- examples 代表你给大模型传入的样例是什么
例如:
python
examples = [
{"input": "我是小明", "output": "小明"},
{
"input": "我是小红",
"output": "小红",
},
{
"input": "我叫小刚",
"output": "小刚",
},
]
我们的案例其实就是从 input 的文字中,提取出名字。
- example_prompt,也就是我们的提示词模板,这个模板是跟上面的 examples 相关的,也就是必须包含 examples 中的字典的 key,在我们案例里是需要包含 input 和 output 。案例如下:
python
from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate
prompt = PromptTemplate.from_template(
template="input: {input} output:{output}",
)
最终 在 FewShotPromptTemplate 中会把上面的案例内容填充进去,也就是自动得到如下文字
lua
input: 我是小明 output:小明\n\ninput: 我是小红 output:小红\n\ninput: 我叫小刚 output:小刚
好了,别忘了上面的 FewShotPromptTemplate 的用法,我
python
few_shot_prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=prompt,
suffix="input:{input} output:", # 声明在示例后面的提示词模板
input_variables=["input"],
)
suffix 就是我们刚才说的,生成的模板字符串是
lua
input: 我是小明 output:小明\n\ninput: 我是小红 output:小红\n\ninput: 我叫小刚 output:小刚
然后,suffix 会在后面拼接,得到(注意最后)
arduino
'input: 我是小明 output:小明\n\ninput: 我是小红 output:小红\n\ninput: 我叫小刚 output:小刚\n\ninput:我叫小华 output:'
你可能奇怪了,为什么上面有个我叫小华 output:,明明 suffix 写的是 input:{input} output:,其实是suffix 中的 input 被变量替换了,如何替换呢?在调用的时候,如下:
ini
res = few_shot_prompt.invoke({"input": "我叫小华"})
此时会把参数替换 suffix 中的占位符。
综合上面,我们来写完整示例:
python 版本:
python
import dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate
from langchain_core.messages import HumanMessage
import os
prompt = PromptTemplate.from_template(
template="input: {input} output:{output}",
)
examples = [
{"input": "我是小明", "output": "小明"},
{
"input": "我是小红",
"output": "小红",
},
{
"input": "我叫小刚",
"output": "小刚",
},
]
few_shot_prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=prompt,
suffix="input:{input} output:", # 声明在示例后面的提示词模板
input_variables=["input"],
)
res = few_shot_prompt.invoke({"input": "我叫小华"})
print(res)
返回结果如下:
ini
text='input: 我是小明 output:小明\n\ninput: 我是小红 output:小红\n\ninput: 我叫小刚 output:小刚\n\ninput:我叫小华 output:'
接着看 node.js 版本:
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "langchain";
import { PromptTemplate, FewShotPromptTemplate } from "@langchain/core/prompts";
dotenv.config();
const prompt = PromptTemplate.fromTemplate("input: {input} output:{output}");
const examples = [
{ input: "我是小明", output: "小明" },
{
input: "我是小红",
output: "小红",
},
{
input: "我叫小刚",
output: "小刚",
},
];
async function printResult() {
const few_shot_prompt = new FewShotPromptTemplate({
examples,
examplePrompt: prompt,
suffix: "input:{input} output:",
inputVariables: ["input"],
});
const res = await few_shot_prompt.invoke({ input: "我叫小华" });
console.log("res: ", res);
}
printResult();
返回结果如下:
css
StringPromptValue {
lc_serializable: true,
lc_kwargs: {
value: '\n' +
'\n' +
'input: 我是小明 output:小明\n' +
'\n' +
'input: 我是小红 output:小红\n' +
'\n' +
'input: 我叫小刚 output:小刚\n' +
'\n' +
'input:我叫小华 output:'
},
lc_namespace: [ 'langchain_core', 'prompt_values' ],
value: '\n' +
'\n' +
'input: 我是小明 output:小明\n' +
'\n' +
'input: 我是小红 output:小红\n' +
'\n' +
'input: 我叫小刚 output:小刚\n' +
'\n' +
'input:我叫小华 output:'
}
可以看到,实际上 value 的值就是拼接了 example 和我们自己的提问.
FewShotChatMessagePromptTemplate
FewShotChatMessagePromptTemplate 也是提供示例的模板,主要配合的是 ChatPromptTemplate 一起使用。
注意:FewShotChatMessagePromptTemplate 里我们只传了 examples 和 example_prompt 参数 我们举例:
python 版本:
python
import dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
from langchain_core.messages import HumanMessage
import os
prompt = ChatPromptTemplate.from_messages([("human", "{input}"), ("ai", "{output}")])
examples = [
{"input": "我是小明", "output": "小明"},
{
"input": "我是小红",
"output": "小红",
},
{
"input": "我叫小刚",
"output": "小刚",
},
]
few_shot_prompt = FewShotChatMessagePromptTemplate(
examples=examples,
example_prompt=prompt,
)
res = few_shot_prompt.invoke({})
print(res)
返回内容如下:
ini
messages=[HumanMessage(content='我是小明', additional_kwargs={}, response_metadata={}), AIMessage(content='小明', additional_kwargs={}, response_metadata={}), HumanMessage(content='我是小红', additional_kwargs={}, response_metadata={}), AIMessage(content='小红', additional_kwargs={}, response_metadata={}), HumanMessage(content='我叫小刚', additional_kwargs={}, response_metadata={}), AIMessage(content='小刚', additional_kwargs={}, response_metadata={})]
其实就是将 examples 的变量,替换 [("human", "{input}"), ("ai", "{output}")] 模板
node.js 示例:
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "langchain";
import {
ChatPromptTemplate,
FewShotChatMessagePromptTemplate,
} from "@langchain/core/prompts";
dotenv.config();
const prompt = ChatPromptTemplate.fromMessages([
["human", "{input}"],
["ai", "{output}"],
]);
const examples = [
{ input: "我是小明", output: "小明" },
{
input: "我是小红",
output: "小红",
},
{
input: "我叫小刚",
output: "小刚",
},
];
async function printResult() {
const few_shot_prompt = new FewShotChatMessagePromptTemplate({
examples,
examplePrompt: prompt,
inputVariables: [],
});
const res = await few_shot_prompt.invoke({});
console.log("res: ", res);
}
printResult();
返回:
javascript
ChatPromptValue {
// ... 删除其它不相关属性。、
messages: [
HumanMessage {
"content": "我是小明",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"content": "小明",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
},
HumanMessage {
"content": "我是小红",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"content": "小红",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
},
HumanMessage {
"content": "我叫小刚",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"content": "小刚",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
}
]
}
Output Parsers
接下来是 Model 最后一个部分,叫输出解析器。因为实际上 AI 返回的格式,我们在不同的环境下可能需要不同的格式,例如直接返回给用户,我们希望使用字符串的格式,例如返回给数据库,可能需要 json 的格式,我们解析后存入数据库。
这里列举比较常用的输出解析器的类型:
- StrOutputParser:字符串解析器
- JsonOutputPrser:JSON解析器,确保输出符合特定的JSON对象格式
- XMLOutputParser: XML解析器,允许以XML格式从大模型返回
- CommaSeparatedListOutputParser: CSV解析器,模型的输出以逗号分割,以列表的形式返回输出
- DatetimeOutputParser: 日期时间解析器,可用于将大模型输出解析为日期时间
我们先来看看最简单的字符串解析器怎么用
StrOutputParser
python 代码:
python
import dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
from langchain_core.messages import HumanMessage
from langchain_core.output_parsers import StrOutputParser
import os
dotenv.load_dotenv()
llm = ChatOpenAI(
model_name=os.getenv("MODEL_NAME"),
temperature=0,
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
res = llm.invoke("你是谁")
print("res:", res)
print("res.content:", res.content)
parser = StrOutputParser()
print("parser.invoke(res):", parser.invoke(res))
注意上面三个打印的内容,
第一个 print("res:", res) 返回
ini
res: content='你好!我是DeepSeek,由深度求索公司创造的AI助手!省略其它文字。。。' additional_kwargs={} 省略其它属性}
第一个 print("res.content:", res.content)
其实就是上面字典中的 content 属性,属于纯文字
除了上面可以获取纯文字,还可以使用 StrOutputParser 获取文字,
其中 print("parser.invoke(res):", parser.invoke(res)) 返回内容跟上面的 print("res.content:", res.content) 是一样的
node.js 案例如下:
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
dotenv.config();
const llm = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
temperature: 0,
openAIApiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL,
});
async function printResult() {
const res = await llm.invoke("你是谁?");
const parser = new StringOutputParser();
const parserResult = await parser.invoke(res);
console.log("res: ", res.content);
console.log("res: ", parserResult);
}
printResult();
注意上面两个个打印的内容, 其中大模型的 res 返回的是一个对象,这个对象有个属性是 content,内容就是大模型返回的文字,所以是字符串。
因此: console.log("res: ", res.content); 返回的是字符串。然后 console.log("res: ", parserResult); 返回的内容一模一样,也是字符串。返回结果如下:
你好!我是DeepSeek,由深度求索公司创造的AI助手!😊 ...省略后面文字
JsonOutputPrser
这说白了就是返回 json 格式。而且 JsonOutputPrser 并没有什么黑魔法让大模型输出 json,其实就是简单的告诉大模型,要生成 json 格式,然后不准有注释这类文字信息。
但是实际生产环境,我们肯定要指定字段,比如返回格式是{ name: xx, age: xx },是我们期望的字段,而不是随便返回。所以还需要使用一些类型定义的库来协助。例如 node.js 中要使用到 zod 这个类型定义的库。我们先来看看 node.js 的版本。
以下是 node.js 版本:
首先使用
javascript
import dotenv from "dotenv";
import { ChatOpenAI } from "@langchain/openai";
import {
ChatPromptTemplate,
} from "@langchain/core/prompts";
import {
StructuredOutputParser,
} from "@langchain/core/output_parsers";
import { z } from "zod";
dotenv.config();
/**
* 1. 定义结构(强烈推荐)
*/
const UserSchema = z.object({
name: z.string().describe("用户姓名"),
age: z.number().describe("用户年龄"),
});
/**
* 2. 创建 Parser
* 本质:JsonOutputParser + Zod 校验
*/
const parser = StructuredOutputParser.fromZodSchema(UserSchema);
/**
* 3. Prompt(必须注入 format instructions)
*/
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个后端 API,请严格返回 JSON.\n${instructions}"],
["human", "{input}"],
]);
/**
* 4. 模型
*/
const llm = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
temperature: 0,
openAIApiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL,
});
async function printResult() {
const template = await prompt.invoke({
input: "生成一个用户对象",
instructions: parser.getFormatInstructions(),
});
console.log("template: ", template);
const res = await llm.invoke(template);
console.log("res: ", res);
}
printResult();
注意,首先我们要定义类型,例如上面的 UserSchema,然后使用
- StructuredOutputParser.fromZodSchema(UserSchema) 生成 json 解析器示例,
最最重要的是 parser.getFormatInstructions() 方法。会生成一类字符串,这个函数返回这样的字符串:
css
你是一个后端 API,请严格返回 JSON.\n$You must format your output as a JSON value that adheres to a given 后面描述我们字段的类型
这就是 json 解析器的本质。
以下是 python 版本:
python
import dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
import os
dotenv.load_dotenv()
# 1. 定义结构 (使用 Pydantic 替代 Zod)
class User(BaseModel):
name: str = Field(description="用户姓名")
age: int = Field(description="用户年龄")
# 2. 创建 Parser
# StructuredOutputParser 在 Python 中通常对应 PydanticOutputParser
parser = PydanticOutputParser(pydantic_object=User)
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个后端 API,请严格返回 JSON.\n{format_instructions}"),
("human", "{input}"),
]
)
llm = ChatOpenAI(
model=os.getenv("MODEL_NAME"), # type: ignore
temperature=1,
api_key=os.getenv("OPENAI_API_KEY"), # type: ignore
base_url=os.getenv("OPENAI_BASE_URL"),
)
prompt_value = prompt.invoke(
{
"input": "生成一个用户对象",
"format_instructions": parser.get_format_instructions(),
}
)
res = llm.invoke(prompt_value)
print("res: ", res)
python 使用 pydantic 库来定义个类型,然后使用 parser = PydanticOutputParser(pydantic_object=User) 创建一个结构化的解析器。
最终调用 parser.get_format_instructions() 方法来返回一个要求大模型返回json格式的字符串。
欢迎一起交流
欢迎大家一起进群讨论前端全栈技术和 ai agent ,一起进步!