Nodejs也能写Agent - 11.LangChain篇 - 模型与Prompt
上一篇我们把 LangChain.js 的地基打完了——搞懂了「一切皆 Runnable」、用 .pipe() 串管道、用 Message 承载对话。但那会儿我们对模型和 Prompt 都还只是「能跑就行」的程度,参数怎么调、模板怎么选,全靠抄示例。
这一篇我们就把这两个每天都要打交道的家伙掰开揉碎讲清楚:模型(Model)负责「思考」,Prompt 负责「你怎么问」。在 LangChain.js 里,我们用 ChatOllama 对接本地 Ollama,用 PromptTemplate / ChatPromptTemplate 管理输入格式。把这两样吃透,你才算真正会「跟模型好好说话」——这是后面写 Agent 的基本功。
老规矩,本文所有 API 都以官网最新文档为准核对过(https://docs.langchain.com/oss/javascript)。LangChain 迭代很快,跟着敲时如果发现某个 API 对不上,请以你当时的官网版本为准。
ChatOllama:本地对话模型
ChatOllama 来自 @langchain/ollama,是我们整个系列的默认模型接口。它面向对话场景,输入输出都是 Message 格式,天然支持多轮对话、Tool calling 和结构化输出——正是写 Agent 需要的能力。
先看最基础的实例化:
import { ChatOllama } from "@langchain/ollama";
const llm = new ChatOllama({
model: "qwen2.5:7b",
baseUrl: "http://localhost:11434",
temperature: 0.7,
});
几个最常用的构造参数:
| 参数 | 说明 | 示例 |
|---|---|---|
model |
模型名称,必须已经 ollama pull 拉下来(含 tag) |
"qwen2.5:7b" |
baseUrl |
Ollama 服务地址,默认就是这个 | "http://localhost:11434" |
temperature |
随机性,0 = 确定性最强,1 = 更有创意 |
0.7 |
numPredict |
最多生成多少 token(相当于输出长度上限) | 512 |
numCtx |
上下文窗口大小(token 数) | 8192 |
⚠️ 一个从 Python 抄代码时极易踩的坑:JS 版的参数是 camelCase!Python 里写
num_predict、num_ctx,到了 JS/TS 里对应的是numPredict、numCtx。抄 Python 教程时记得转一下驼峰,否则参数会静默失效。
实际项目里,我更推荐把这些配置放进环境变量,而不是硬编码:
const llm = new ChatOllama({
model: process.env.OLLAMA_MODEL ?? "qwen2.5:7b",
baseUrl: process.env.OLLAMA_BASE_URL ?? "http://localhost:11434",
temperature: 0.7,
});
这样本地开发用 Ollama、线上换成别的服务时,改 .env 就行,代码一个字不用动。
ChatOllama vs Ollama:别用错了类
@langchain/ollama 其实提供了两个模型类,很多人(尤其是从 Python 转过来的)会在这里搞混:
| 类 | 端点 | 适用场景 | Tool calling |
|---|---|---|---|
ChatOllama |
/api/chat |
多轮对话、Agent、RAG(几乎所有现代场景) | ✅ 支持 |
Ollama |
/api/generate |
纯文本补全(续写、单轮补全) | ❌ 不支持 |
⚠️ 这是 JS 和 Python 的一个命名差异,务必注意:
补全式模型这个类,在 Python 里叫OllamaLLM,但在 JavaScript 里叫Ollama(import { Ollama } from "@langchain/ollama")。网上不少中文教程直接照抄 Python 写成OllamaLLM,在 JS 里是导入不到的。结论很简单:现代应用一律用
ChatOllama。Ollama(补全式)只在你要做「续写一段文本」这种纯补全任务时才用得上,我们整个系列基本不会碰它。
Prompt 模板:PromptTemplate vs ChatPromptTemplate
模型是「大脑」,那 Prompt 就是你递给大脑的「题面」。你当然可以每次手写字符串,但一旦要插入变量、复用模板、管理多轮消息,手拼字符串很快就会变成噩梦。LangChain 提供了两种模板,对应两类模型:
PromptTemplate(字符串模板)
用于补全式模型(也就是上面说的 Ollama),最终产出一个纯字符串 Prompt:
import { PromptTemplate } from "@langchain/core/prompts";
const template = PromptTemplate.fromTemplate("将以下文本翻译成英文:{text}");
const formatted = await template.format({ text: "你好世界" });
// → "将以下文本翻译成英文:你好世界"
说白了就是个带占位符的字符串模板,{text} 会被替换掉。日常用得少,了解即可。
ChatPromptTemplate(Message 模板)
用于对话式模型(ChatOllama),产出的是 Message 数组——这才是我们的主力:
import { ChatPromptTemplate } from "@langchain/core/prompts";
const chatPrompt = ChatPromptTemplate.fromMessages([
["system", "你是专业翻译,只输出译文,不加任何解释。"],
["human", "请将以下中文翻译为英文:{text}"],
]);
fromMessages 里的每一项可以是元组 [role, content],也可以是预先构建好的 Message 对象。角色字符串 "system" / "human" / "ai" 会自动映射成对应的 SystemMessage / HumanMessage / AIMessage。
partial:先填一半变量
有时候某些变量你早就知道(比如 AI 的角色设定),不想每次调用都重复传。可以用 .partial() 提前把它填掉:
const chatPrompt = ChatPromptTemplate.fromMessages([
["system", "你是一位{role}。"],
["human", "{input}"],
]);
// 提前把 role 固定住,之后只需要传 input
const translatorPrompt = await chatPrompt.partial({ role: "专业翻译" });
const messages = await translatorPrompt.invoke({ input: "翻译:你好" });
MessagesPlaceholder:给历史消息留个「坑位」
这个非常关键,也是通往「多轮记忆」和「Agent」的钥匙。做聊天机器人时,你需要把一整段历史对话动态塞进 Prompt。硬编码显然不行,MessagesPlaceholder 就是专门干这个的——它在模板里占一个位置,运行时你再把一个 Message 数组灌进去:
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个友好的助手。"],
new MessagesPlaceholder("history"), // 历史消息的坑位
["human", "{input}"],
]);
// 也可以用简写形式:["placeholder", "{history}"]
调用时把之前的对话记录整个传给 history 就行。等我们后面讲多轮记忆和 Agent 时,你会反复见到它。
invoke vs stream:一次性拿 vs 逐字蹦
同一个模型或同一条链,有两种主要的调用姿势:
| 模式 | 方法 | 返回 | 适用场景 |
|---|---|---|---|
| 一次性 | invoke(input) |
完整的 AIMessage 或链的最终输出 |
后台批处理、短回复 |
| 流式 | stream(input) |
AsyncIterable,逐块(chunk)产出 |
聊天 UI、长文本生成 |
区别很直观:invoke 会等模型把话说完才一次性返回;stream 则是每生成一小块就立刻吐给你,前端可以像打字机一样实时渲染。真实产品里的聊天界面,几乎清一色用流式——没人愿意对着转圈圈干等十几秒。
因为「一切皆 Runnable」,invoke 和 stream 对整条链同样适用:
const chain = prompt.pipe(llm).pipe(new StringOutputParser());
// 一次性
await chain.invoke({ topic: "AI" });
// 流式:逐字输出
for await (const chunk of await chain.stream({ topic: "AI" })) {
process.stdout.write(chunk);
}
Ollama 本地配置的几个要点
跟着敲之前,这几个坑先给你打个预防针:
- 确保 Ollama 在跑:它默认监听
11434端口。装完一般会自启,拿不准就命令行敲ollama list看看。 - 模型名要和 pull 的完全一致:包括 tag。你
ollama pull qwen2.5:7b,代码里model就得是"qwen2.5:7b",少个:7b都可能找不到。 - temperature 按场景调:代码生成、结构化输出这类要「稳」的任务建议
0 ~ 0.3;创意写作可以拉到0.7 ~ 1.0。 - 注意上下文长度:本地小模型的 context window 有限,Prompt 太长会被截断。做 RAG 时尤其要控制塞进去的检索片段数量,必要时用
numCtx调大窗口(但会更吃显存)。 baseUrl末尾别带斜杠:写http://localhost:11434,别写成.../,某些版本拼接路径时会出问题。
代码示例
示例 1:ChatPromptTemplate + invoke
一个完整可跑的「模板 → 模型 → 解析器」链路:
import { ChatOllama } from "@langchain/ollama";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
// 带变量的对话模板
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一位 TypeScript 专家。回答简洁,并附带简短代码示例。"],
["human", "请解释 {concept} 的用途。"],
]);
const llm = new ChatOllama({
model: "qwen2.5:7b",
baseUrl: "http://localhost:11434",
temperature: 0.2, // 讲技术,调低一点更稳
});
const chain = prompt.pipe(llm).pipe(new StringOutputParser());
const result = await chain.invoke({ concept: "泛型 Generics" });
console.log(result);
示例 2:stream 流式输出
聊天场景必备。注意这里演示的是直接对模型 stream,拿到的 chunk 是 AIMessageChunk(不是纯字符串),它的 content 需要自己取:
import { ChatOllama } from "@langchain/ollama";
import { HumanMessage } from "@langchain/core/messages";
const llm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0.7 });
const stream = await llm.stream([
new HumanMessage("用五句话介绍 LangGraph.js 的核心价值。"),
]);
process.stdout.write("AI: ");
for await (const chunk of stream) {
// chunk 是 AIMessageChunk,content 可能是 string 或复杂结构
const text =
typeof chunk.content === "string"
? chunk.content
: JSON.stringify(chunk.content);
process.stdout.write(text);
}
console.log();
小提示:如果你在链尾接了
StringOutputParser(像示例 1 那样),再对整条链stream,那拿到的 chunk 就已经是纯string了,不用自己解content。解析器帮你把这一步做掉了。
示例 3:用 MessagesPlaceholder 实现多轮对话
把「历史消息」这个坑位用起来,就能手动维护一段对话记忆:
import { ChatOllama } from "@langchain/ollama";
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一位耐心的 Node.js 导师,用中文回答。"],
new MessagesPlaceholder("history"),
["human", "{input}"],
]);
const llm = new ChatOllama({ model: "qwen2.5:7b" });
const chain = prompt.pipe(llm);
// 手动攒一份历史记录
const history = [
new HumanMessage("什么是事件循环?"),
new AIMessage("事件循环是 Node.js 处理异步任务的核心机制……"),
];
const res = await chain.invoke({
history,
input: "那它和浏览器里的事件循环有什么不同?",
});
console.log(res.content);
这里我们是「手动」传历史;等后面进入 LangGraph,记忆(Checkpointer)会帮我们把这件事自动化。
常见坑
我把最容易翻车的几点单独拎出来,帮你少走弯路:
- 对话场景误用
Ollama(补全式):多轮对话、Tool calling 请用ChatOllama;Ollama只适合纯文本补全。而且再强调一遍——JS 里补全类叫Ollama,不是 Python 的OllamaLLM。 - 模板变量名对不上:模板里写
{topic},调用却传invoke({ subject: "..." }),变量不会被替换,甚至直接报错。名字必须一一对应。 - 忘记
await:invoke/stream返回的都是 Promise,漏写await你会拿到一个 Pending 的 Promise 而不是结果。 - 参数用了下划线:JS 里是
numPredict、numCtx,不是num_predict、num_ctx。抄 Python 代码时最容易中招。 PromptTemplate硬接ChatOllama:PromptTemplate产出的是字符串,直接喂给对话模型语义上并不匹配。对话场景请统一用ChatPromptTemplate,省心。
更多推荐

所有评论(0)