Nodejs也能写Agent - 11.LangChain篇 - 模型与Prompt

📅 2026/7/11 7:57:52
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}fromlangchain/ollama;constllmnewChatOllama({model:qwen2.5:7b,baseUrl:http://localhost:11434,temperature:0.7,});几个最常用的构造参数参数说明示例model模型名称必须已经ollama pull拉下来含 tagqwen2.5:7bbaseUrlOllama 服务地址默认就是这个http://localhost:11434temperature随机性0 确定性最强1 更有创意0.7numPredict最多生成多少 token相当于输出长度上限512numCtx上下文窗口大小token 数8192⚠️一个从 Python 抄代码时极易踩的坑JS 版的参数是camelCasePython 里写num_predict、num_ctx到了 JS/TS 里对应的是numPredict、numCtx。抄 Python 教程时记得转一下驼峰否则参数会静默失效。实际项目里我更推荐把这些配置放进环境变量而不是硬编码constllmnewChatOllama({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 callingChatOllama/api/chat多轮对话、Agent、RAG几乎所有现代场景✅ 支持Ollama/api/generate纯文本补全续写、单轮补全❌ 不支持⚠️这是 JS 和 Python 的一个命名差异务必注意补全式模型这个类在 Python 里叫OllamaLLM但在 JavaScript 里叫Ollamaimport { Ollama } from langchain/ollama。网上不少中文教程直接照抄 Python 写成OllamaLLM在 JS 里是导入不到的。结论很简单现代应用一律用ChatOllama。Ollama补全式只在你要做「续写一段文本」这种纯补全任务时才用得上我们整个系列基本不会碰它。Prompt 模板PromptTemplate vs ChatPromptTemplate模型是「大脑」那 Prompt 就是你递给大脑的「题面」。你当然可以每次手写字符串但一旦要插入变量、复用模板、管理多轮消息手拼字符串很快就会变成噩梦。LangChain 提供了两种模板对应两类模型PromptTemplate字符串模板用于补全式模型也就是上面说的Ollama最终产出一个纯字符串 Promptimport{PromptTemplate}fromlangchain/core/prompts;consttemplatePromptTemplate.fromTemplate(将以下文本翻译成英文{text});constformattedawaittemplate.format({text:你好世界});// → 将以下文本翻译成英文你好世界说白了就是个带占位符的字符串模板{text}会被替换掉。日常用得少了解即可。ChatPromptTemplateMessage 模板用于对话式模型ChatOllama产出的是 Message 数组——这才是我们的主力import{ChatPromptTemplate}fromlangchain/core/prompts;constchatPromptChatPromptTemplate.fromMessages([[system,你是专业翻译只输出译文不加任何解释。],[human,请将以下中文翻译为英文{text}],]);fromMessages里的每一项可以是元组[role, content]也可以是预先构建好的 Message 对象。角色字符串system/human/ai会自动映射成对应的SystemMessage/HumanMessage/AIMessage。partial先填一半变量有时候某些变量你早就知道比如 AI 的角色设定不想每次调用都重复传。可以用.partial()提前把它填掉constchatPromptChatPromptTemplate.fromMessages([[system,你是一位{role}。],[human,{input}],]);// 提前把 role 固定住之后只需要传 inputconsttranslatorPromptawaitchatPrompt.partial({role:专业翻译});constmessagesawaittranslatorPrompt.invoke({input:翻译你好});MessagesPlaceholder给历史消息留个「坑位」这个非常关键也是通往「多轮记忆」和「Agent」的钥匙。做聊天机器人时你需要把一整段历史对话动态塞进 Prompt。硬编码显然不行MessagesPlaceholder就是专门干这个的——它在模板里占一个位置运行时你再把一个 Message 数组灌进去import{ChatPromptTemplate,MessagesPlaceholder,}fromlangchain/core/prompts;constpromptChatPromptTemplate.fromMessages([[system,你是一个友好的助手。],newMessagesPlaceholder(history),// 历史消息的坑位[human,{input}],]);// 也可以用简写形式[placeholder, {history}]调用时把之前的对话记录整个传给history就行。等我们后面讲多轮记忆和 Agent 时你会反复见到它。invoke vs stream一次性拿 vs 逐字蹦同一个模型或同一条链有两种主要的调用姿势模式方法返回适用场景一次性invoke(input)完整的AIMessage或链的最终输出后台批处理、短回复流式stream(input)AsyncIterable逐块chunk产出聊天 UI、长文本生成区别很直观invoke会等模型把话说完才一次性返回stream则是每生成一小块就立刻吐给你前端可以像打字机一样实时渲染。真实产品里的聊天界面几乎清一色用流式——没人愿意对着转圈圈干等十几秒。因为「一切皆 Runnable」invoke和stream对整条链同样适用constchainprompt.pipe(llm).pipe(newStringOutputParser());// 一次性awaitchain.invoke({topic:AI});// 流式逐字输出forawait(constchunkofawaitchain.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别写成.../某些版本拼接路径时会出问题。代码示例示例 1ChatPromptTemplate invoke一个完整可跑的「模板 → 模型 → 解析器」链路import{ChatOllama}fromlangchain/ollama;import{ChatPromptTemplate}fromlangchain/core/prompts;import{StringOutputParser}fromlangchain/core/output_parsers;// 带变量的对话模板constpromptChatPromptTemplate.fromMessages([[system,你是一位 TypeScript 专家。回答简洁并附带简短代码示例。],[human,请解释 {concept} 的用途。],]);constllmnewChatOllama({model:qwen2.5:7b,baseUrl:http://localhost:11434,temperature:0.2,// 讲技术调低一点更稳});constchainprompt.pipe(llm).pipe(newStringOutputParser());constresultawaitchain.invoke({concept:泛型 Generics});console.log(result);示例 2stream 流式输出聊天场景必备。注意这里演示的是直接对模型stream拿到的 chunk 是AIMessageChunk不是纯字符串它的content需要自己取import{ChatOllama}fromlangchain/ollama;import{HumanMessage}fromlangchain/core/messages;constllmnewChatOllama({model:qwen2.5:7b,temperature:0.7});conststreamawaitllm.stream([newHumanMessage(用五句话介绍 LangGraph.js 的核心价值。),]);process.stdout.write(AI: );forawait(constchunkofstream){// chunk 是 AIMessageChunkcontent 可能是 string 或复杂结构consttexttypeofchunk.contentstring?chunk.content:JSON.stringify(chunk.content);process.stdout.write(text);}console.log();小提示如果你在链尾接了StringOutputParser像示例 1 那样再对整条链stream那拿到的 chunk 就已经是纯string了不用自己解content。解析器帮你把这一步做掉了。示例 3用 MessagesPlaceholder 实现多轮对话把「历史消息」这个坑位用起来就能手动维护一段对话记忆import{ChatOllama}fromlangchain/ollama;import{ChatPromptTemplate,MessagesPlaceholder,}fromlangchain/core/prompts;import{HumanMessage,AIMessage}fromlangchain/core/messages;constpromptChatPromptTemplate.fromMessages([[system,你是一位耐心的 Node.js 导师用中文回答。],newMessagesPlaceholder(history),[human,{input}],]);constllmnewChatOllama({model:qwen2.5:7b});constchainprompt.pipe(llm);// 手动攒一份历史记录consthistory[newHumanMessage(什么是事件循环),newAIMessage(事件循环是 Node.js 处理异步任务的核心机制……),];constresawaitchain.invoke({history,input:那它和浏览器里的事件循环有什么不同,});console.log(res.content);这里我们是「手动」传历史等后面进入 LangGraph记忆Checkpointer会帮我们把这件事自动化。常见坑我把最容易翻车的几点单独拎出来帮你少走弯路对话场景误用Ollama补全式多轮对话、Tool calling 请用ChatOllamaOllama只适合纯文本补全。而且再强调一遍——JS 里补全类叫Ollama不是 Python 的OllamaLLM。模板变量名对不上模板里写{topic}调用却传invoke({ subject: ... })变量不会被替换甚至直接报错。名字必须一一对应。忘记awaitinvoke/stream返回的都是 Promise漏写await你会拿到一个 Pending 的 Promise 而不是结果。参数用了下划线JS 里是numPredict、numCtx不是num_predict、num_ctx。抄 Python 代码时最容易中招。PromptTemplate硬接ChatOllamaPromptTemplate产出的是字符串直接喂给对话模型语义上并不匹配。对话场景请统一用ChatPromptTemplate省心。