上一篇我们把 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_predictnum_ctx,到了 JS/TS 里对应的是 numPredictnumCtx。抄 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 里叫 Ollamaimport { Ollama } from "@langchain/ollama")。网上不少中文教程直接照抄 Python 写成 OllamaLLM,在 JS 里是导入不到的。

结论很简单:现代应用一律用 ChatOllamaOllama(补全式)只在你要做「续写一段文本」这种纯补全任务时才用得上,我们整个系列基本不会碰它。

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」,invokestream整条链同样适用:

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 本地配置的几个要点

跟着敲之前,这几个坑先给你打个预防针:

  1. 确保 Ollama 在跑:它默认监听 11434 端口。装完一般会自启,拿不准就命令行敲 ollama list 看看。
  2. 模型名要和 pull 的完全一致:包括 tag。你 ollama pull qwen2.5:7b,代码里 model 就得是 "qwen2.5:7b",少个 :7b 都可能找不到。
  3. temperature 按场景调:代码生成、结构化输出这类要「稳」的任务建议 0 ~ 0.3;创意写作可以拉到 0.7 ~ 1.0
  4. 注意上下文长度:本地小模型的 context window 有限,Prompt 太长会被截断。做 RAG 时尤其要控制塞进去的检索片段数量,必要时用 numCtx 调大窗口(但会更吃显存)。
  5. 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)会帮我们把这件事自动化。

常见坑

我把最容易翻车的几点单独拎出来,帮你少走弯路:

  1. 对话场景误用 Ollama(补全式):多轮对话、Tool calling 请用 ChatOllamaOllama 只适合纯文本补全。而且再强调一遍——JS 里补全类叫 Ollama,不是 Python 的 OllamaLLM
  2. 模板变量名对不上:模板里写 {topic},调用却传 invoke({ subject: "..." }),变量不会被替换,甚至直接报错。名字必须一一对应。
  3. 忘记 awaitinvoke / stream 返回的都是 Promise,漏写 await 你会拿到一个 Pending 的 Promise 而不是结果。
  4. 参数用了下划线:JS 里是 numPredictnumCtx,不是 num_predictnum_ctx。抄 Python 代码时最容易中招。
  5. PromptTemplate 硬接 ChatOllamaPromptTemplate 产出的是字符串,直接喂给对话模型语义上并不匹配。对话场景请统一用 ChatPromptTemplate,省心。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐