typescript
php
import 'dotenv/config';
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from '@langchain/core/output_parsers';
import { PromptTemplate } from '@langchain/core/prompts';
// 创意模型------温度拉高,Top-K 收窄
const creativeModel = new ChatOpenAI({
model: 'deepseek-v4-pro',
temperature: 0.8,
topK: 4,
maxToken: 600,
apiKey: process.env.DEEPSEEK_API_KEY,
configuration: { baseURL: 'https://api.deepseek.com' }
});
// 严谨模型------温度压低,Top-K 放开
const preciseModel = new ChatOpenAI({
model: 'deepseek-v4-pro',
temperature: 0.2,
topK: 8,
maxToken: 600,
apiKey: process.env.DEEPSEEK_API_KEY,
configuration: { baseURL: 'https://api.deepseek.com' }
});
// Prompt 模板化------可复用、可维护
const storyPrompt = PromptTemplate.fromTemplate(`
请写一篇短片散文,主题: {theme}
风格温柔治愈,篇幅150字左右,不要分段,文字细腻又有画面感。
`);
const outputParser = new StringOutputParser();
// 两条工作流(Chain)------同一套 Prompt,不同的模型参数,不同的"性格"
const creativeChain = storyPrompt.pipe(creativeModel).pipe(outputParser);
const preciseChain = storyPrompt.pipe(preciseModel).pipe(outputParser);
async function runWriteDemo() {
const theme = "杀手辣条和王sir的故事";
console.log('创意写作模式开启');
const creativeText = await creativeChain.invoke({ theme });
console.log(creativeText);
console.log('严谨写作模式开启');
const preciseText = await preciseChain.invoke({ theme });
console.log(preciseText);
}
runWriteDemo().catch(err => console.error(err));
今天这篇文章,我们不只讲"怎么调参",更重要的是带着源码阅读的思维,把每一个设计决策背后的"为什么"挖出来 start -> llm -> PromptTemplate -> StringOutputParser -> end。
一、核心概念:AI 工作流不是"调 API",而是"搭流水线"
如果你把上面的代码抽象成一张图,它长这样:

它的核心设计初衷可以用一句话概括:
将"调用大模型"这件事,从硬编码的 API 调用 ,升级为可编排、可复用、可观测的 AI 工作流(Chain) 。
我们在代码里做了三件事:
- 模型配置参数化 ------
temperature和topK不再是玄学,而是业务语义的一部分(创意 vs 严谨)。 - Prompt 模板化 ------ 提示词从字符串常量升级为
PromptTemplate,未来换主题、换风格只需改模板,不用动业务逻辑。 - 输出解析标准化 ------
StringOutputParser统一了返回格式,避免手写res.choices[0].message.content这种样板代码。
这就像把"做饭"从"每次重新切菜、调火候"升级成了"预制菜流水线"------工序固定,配料可换,火候可调。
二、痛点与场景:为什么我们不直接调 API?
很多刚接触 AI 应用开发的同学会觉得:这段代码不就是 fetch 调了个接口吗?包这么多层有什么意义?
这么想就踩了"AI 工程化"的第一个坑。
在实际业务中,一个 AI 功能从来不是"调一次模型就完事"。真实场景往往是:
| 业务需求 | 直接调 API 的问题 | 工作流方案 |
|---|---|---|
| 同一个 Prompt 要同时服务创意风和严谨风 | 复制粘贴代码,维护两份 | 同一套 Prompt + 不同模型参数 |
| 后来产品说要改成"伤感风""悬疑风" | 改代码、重新发版 | 只改 PromptTemplate 模板内容 |
| 要记录每次请求的 Token 消耗和耗时 | 手动埋点,侵入业务逻辑 | Chain 中间件统一拦截 |
| 后续要接入 RAG(检索增强) | 代码被改得面目全非 | 在 Chain 中插入 retriever 节点 |
| 要支持多轮对话(Message History) | 自己维护对话数组 | LangChain 的 MessagesPlaceholder |
所以,这段代码解决的根本痛点是:
AI 应用开发不是"调通 API"就结束了,而是需要一套标准化的"工作流编排框架",让业务逻辑和 AI 能力解耦,让代码经得起需求变更。 这也是 LangChain 名字的来源------Lang (uage) + Chain(链/工作流)。
三、重难点剖析(核心)
下面进入全文最硬核的部分。我们不看表面,看设计决策。
难点一:大模型是怎么"随机说话"的?Temperature、Top-K、Top-P 的"缺陷-互补-升华"
面试官真正想问的是:你知不知道大模型生成文本时,确定性(逻辑推理)和随机性(创造发挥)是如何平衡的?
数学本质:它不是"随机",而是"采样"
大模型本质上是一个概率预测器。它每次只预测下一个词(Token)是什么,并给词表中所有词赋予一个概率分布。
- 输入
1+1=,模型输出2的概率是 99.9%,其他词概率极低------这是确定性。 - 输入"写一首诗",模型输出
春的概率是 15%,秋的概率是 13%,夜的概率是 10%......------这是不确定性。
"随机说话"就是按照这个概率分布进行抽样(Sample) ,而不是每次都取最高分(贪心解码)。
第一步:先戳"单一使用"的致命伤
缺陷 1:只用 Temperature------尾大不掉,垃圾里淘金
现象 :无论你把温度调得多低(比如 temperature=0.01),数学上所有词的概率永远不会等于零,只是无限趋近于零。
后果 :生成一个短句(20 个字)没问题,但生成一篇长文(1000 个字)时,那成千上万个低概率的"生僻字"或"不搭边词"的总累积概率依然可观。模型会在某个瞬间"抽风",蹦出一个毫无逻辑的乱码词。
本质 :只调温度,相当于把噪音调小,但噪音永远在喇叭里。你无法根治"胡说八道"的底层风险。
缺陷 2:只用 Top-K------削足适履,僵化死板
规则:不管概率是多少,只看排名。规定只留前 K 个,剩下的无论多靠谱,一律淘汰。
特点 :候选人数是固定的(比如 K=50,永远留 50 个)。
缺陷:很死板,不会看语境"脸色"。
| 场景 | 问题 |
|---|---|
1+1= 确定语境 |
可能只有 2 个词靠谱,但你非要留 50 个,结果把 3、4 这种错误答案也放进了抽奖池 |
| 写诗发散语境 | 可能有 300 个词意境都不错,但你只留 50 个,把剩下的 250 个"好词"全扼杀了 |
本质:Top-K 是"一刀切"的物理切割,它没有"审美"能力,不知道当前语境是该"收窄"还是"放宽"。
缺陷 3:只用 Top-P 的潜在问题(虽然它已经比 Top-K 聪明)
规则:不管人数多少,只管总概率。按概率从高到低累加,直到累加的总和达到 P(比如 0.9)就停,把这堆词留下。
特点 :候选人数是动态变化的,完全由上下文决定。
潜在风险 :当概率分布极其分散时(比如创意写作场景),Top-P 算出来的候选池可能有 5000 个词,这会严重拖慢推理速度。
第二步:组合拳如何"负负得正"------概率采样的"缺陷-互补-升华"

第三步:三大参数的完整对比(面试直接用这张表)
| 维度 | Temperature(温度) | Top-K | Top-P(核采样) |
|---|---|---|---|
| 作用对象 | 整个概率分布的平滑度 | 候选词的数量(排名) | 候选词的概率质量(累加和) |
| 筛选依据 | 缩放 logits,重塑分布 | 按排名切 | 按概率累加和切 |
| 候选池大小 | 不变(仍是全词表) | 固定(永远是 K 个) | 动态(可能是 1 个,也可能是 500 个) |
| 极端场景表现 | 低温仍有噪音,高温完全失控 | 确定性下留太多废词,发散性下砍太多好词 | 概率分散时候选池过大,拖慢速度 |
| 核心问题 | 治标不治本,噪音永远在 | 削足适履,不懂语境 | 性能兜底不足 |
| 工业界定位 | 调节"胆量" | 辅助/兜底 | 默认首选 |
第四步:工业级"三板斧"------它们到底怎么配合的?
实际生产(比如 ChatGPT、Claude)中,不是二选一,而是组合拳:
先调温度(改权重) → 再 Top-P(动态画圈) → 最后 Top-K(兜底安全阀)
完整工作流:

为什么要加 Top-K 兜底?
因为有时候 Top-P 算出来的候选池可能有 5000 个词(当概率极其分散时),这一步会拖慢计算速度。所以会加一个 Top-K 作为上限,比如 K=100------ "最多只留 100 个,哪怕 Top-P 算出来要留 500 个,我也只取前 100 个" 。
工程推荐配置:
| 场景 | Temperature | Top-P | Top-K(兜底) |
|---|---|---|---|
| 代码生成 / 数学推理 | 0.1 - 0.2 | 0.9 | 50 |
| 创意写作 / 头脑风暴 | 0.8 - 1.0 | 0.95 | 100 |
| 客服对话 / 摘要总结 | 0.3 - 0.5 | 0.95 | 80 |
第五步:面试终极话术(直接背)
"Temperature、Top-K、Top-P 三者有明确的分工:
- Temperature 负责'胆量'------通过缩放概率分布,决定模型敢不敢尝试概率稍低的词。低温让分布尖锐,模型保守稳定;高温让分布平坦,模型天马行空。
- Top-P 负责'边界'------通过动态累加概率质量,根据上下文自动伸缩候选池。确定场景下自动收窄,发散场景下自动放宽。工业界默认首选 Top-P,因为它的阈值 P 有明确的物理意义:'考虑前 90% 的合理可能性'。
- Top-K 负责'兜底'------作为性能安全阀,防止 Top-P 在极端情况下候选池过大拖慢推理速度。
从'缺陷-互补-升华'的视角看:单用 Temperature 治标不治本,噪音永远在喇叭里;单用 Top-K 僵硬死板,不懂上下文的收放;单用 Top-P 虽然聪明,但极端情况下性能不可控。三者组合后,Temperature 负责'幅度整形',Top-P 负责'动态画圈',Top-K 负责'性能兜底'------实现了概率质量重新分配、动态适应上下文、创意与稳定解耦、性能与质量平衡的四重收益。
这就像拍电影:Temperature 是导演的审美倾向(决定镜头该柔和还是锐利),Top-P 是摄影师的取景框(根据现场光线动态决定该拍特写还是全景),Top-K 是制片人的预算上限(防止取景框无限扩大拖垮制作进度)。只有三者协同,才能保证每一帧既有艺术追求,又绝不放闲杂人等入镜,还能按时杀青。"
这套回答把数学本质、工程策略、缺陷对比、配合机制、实战配置全讲透了。面试官听完绝对认可你的工程功底!🚀
第六步:代码中的设计决策------为什么是 topK: 4 和 topK: 8?
回到我们的代码:
typescript
arduino
// 创意模型:高 temp + 小 topK
const creativeModel = new ChatOpenAI({
temperature: 0.8, // 高温度,鼓励发散
topK: 4, // 小 K,只从极少数高概率词中采样,防止跑偏
});
// 严谨模型:低 temp + 大 topK
const preciseModel = new ChatOpenAI({
temperature: 0.2, // 低温度,保守稳定
topK: 8, // 大 K,保留更多候选,保证信息完整性
});
这里的设计逻辑是:
| 模型 | 策略 | 为什么这么配? |
|---|---|---|
| 创意模型 | 高 temp + 小 topK | 温度拉高让概率分布变平坦,但用极小的 Top-K 强行把候选圈定在最靠谱的几个词里------在安全范围内"戴着镣铐跳舞" |
| 严谨模型 | 低 temp + 大 topK | 温度压低让分布尖锐,但用较大的 Top-K 保留更多候选------防止因过度自信而遗漏正确答案 |
如果换成 Top-P 的配置方式,可能是:
typescript
php
// 创意模型(Top-P 版本)
const creativeModel = new ChatOpenAI({
temperature: 0.85,
topP: 0.92, // Top-P 动态截断
topK: 80, // Top-K 作为性能兜底
});
难点二:为什么用 PromptTemplate 而不直接拼接字符串?
这段代码里,Prompt 被封装成了 PromptTemplate,而不是直接写在 invoke 里:
typescript
ini
// ❌ 不推荐:硬编码
const response = await model.invoke(`请写一篇短文,主题: ${theme}`);
// ✅ 推荐:模板化
const storyPrompt = PromptTemplate.fromTemplate(`请写一篇短文,主题: {theme}`);
const chain = storyPrompt.pipe(model).pipe(parser);
设计者"为什么这么写"?
这是 LangChain 最核心的设计理念之一:Prompt 是第一公民。
在传统软件开发中,代码逻辑是固定的,输入是数据。但在 AI 应用中,Prompt 本身就是"代码"的一部分------它决定了模型的行为边界、输出格式、角色扮演,甚至安全性。
把 Prompt 做成 PromptTemplate 带来了三个工程化收益:
- 复用性:同一个模板可以注入不同的变量,生成不同的 Prompt。
- 可观测性 :可以在 Chain 的中间节点打印
storyPrompt.format({ theme })查看最终发送给模型的完整 Prompt,方便调试。 - 可组合性 :未来可以组合多个
PromptTemplate(如 SystemPrompt + UserPrompt + 历史消息),这是构建 Agent 的基础。
一句话总结:
PromptTemplate让 Prompt 从"字符串"升级为"可管理、可追溯、可组合的工程资产"。
难点三:StringOutputParser 是不是多此一举?我直接取 content 不就行了?
如果你只看最终效果,确实:await model.invoke() 返回的对象里本来就有 content 字段。
typescript
css
// 裸调返回的结构
{
content: "杀手辣条和王sir的故事......",
additional_kwargs: { ... },
response_metadata: { ... }
}
那为什么要包一层 StringOutputParser?
设计者"为什么这么写"?
这里藏着 LangChain 对职责分离(Separation of Concerns) 的深度坚持:
- Model(模型) 的职责是"与大模型通信",返回原始响应对象。
- Parser(解析器) 的职责是"将原始响应转换为业务层需要的数据格式"。
这样做的好处是:
- 当模型换为 Anthropic Claude 或 Google Gemini 时,返回对象结构不同,但只要换成对应的 Parser,Chain 上层业务代码一行都不用改。
- 当需要解析 JSON 、XML 、结构化数据 时,只需换
JsonOutputParser或XMLOutputParser,Chain 的其他部分保持不变。
StringOutputParser不是多余的中间层,而是"抽象边界"------它让业务代码与底层模型的具体返回格式解耦。
四、避坑指南 / 最佳实践
坑 1:temperature 和 topK 同时拉到极端
| 错误组合 | 后果 |
|---|---|
temperature: 1.0, topK: 50 |
输出接近随机,内容不可控,大概率出现"幻觉" |
temperature: 0.0, topK: 1 |
每次都选概率最高的词,输出极其重复、机械 |
正确姿势:根据业务场景选择"高 temp + 小 topK"或"低 temp + 大 topK",不要两个同时极端。
坑 2:混淆 Top-K 和 Top-P 的适用场景
| 错误用法 | 问题 | 正确做法 |
|---|---|---|
| 所有场景都用固定 K=50 | 确定性场景引入噪音,发散场景扼杀灵感 | 优先使用 Top-P,让候选池动态伸缩 |
| 只用 Top-P 不设 Top-K 兜底 | 概率分散时候选池过大,拖慢推理速度 | 设置 Top-K=100 作为性能安全阀 |
| 在有明确正确答案的任务中用高 Temperature | 模型可能"创造性"地答错数学题 | 代码生成/数学任务用低温(0.1-0.2) |
坑 3:在同一个 Chain 里复用可变状态的 Model 实例
typescript
ini
// ❌ 错误:同一个 model 实例被多个请求共享,且状态可变
const model = new ChatOpenAI({ temperature: 0.5 });
const chain1 = prompt1.pipe(model).pipe(parser);
const chain2 = prompt2.pipe(model).pipe(parser);
// model 的 temperature 在 chain1 中可能被修改,影响 chain2
正确姿势 :不同的配置需求,创建不同的 Model 实例(如本文代码所示),或者使用 .bind() 方法覆盖参数:
typescript
ini
const baseModel = new ChatOpenAI({ model: 'deepseek-v4-pro' });
const creativeModel = baseModel.bind({ temperature: 0.8, topK: 4 });
const preciseModel = baseModel.bind({ temperature: 0.2, topK: 8 });
坑 4:忽略环境变量加载顺序
typescript
javascript
// ❌ 错误:在 import 'dotenv/config' 之前使用了 process.env
import { ChatOpenAI } from "@langchain/openai";
// 此时 dotenv 还没加载,apiKey 为 undefined
正确姿势 :确保 dotenv/config 在最顶部导入,或者在项目入口最早加载。
坑 5:忘记固定随机种子导致结果不可复现
typescript
php
// ✅ 推荐:固定 seed 保证可复现
const model = new ChatOpenAI({
model: 'deepseek-v4-pro',
temperature: 0.8,
topP: 0.95,
seed: 42, // 固定种子
});
为什么重要 :在调试和测试时,如果每次运行结果都不同,根本无法判断是 Prompt 改对了还是"运气好"。固定种子后,同样的输入必然产生同样的输出,这才符合工程化的可复现要求。
坑 6:认为 Top-K 和 Top-P 只能二选一
错误认知:很多同学以为 Top-K 和 Top-P 是互斥的,要么用这个要么用那个。
正确认知 :两者可以同时使用,且工业界正是这么做的------Top-P 负责动态画圈,Top-K 负责性能兜底,两者互补而非互斥。
五、面试高频考点
Q1:Top-K 和 Top-P 的本质区别是什么?工业界更推荐用哪个?
回答要点:
核心区别一句话:Top-K 是"掐头",固定砍掉人数;Top-P 是"掐尾",动态砍掉概率。
| 维度 | Top-K | Top-P(核采样) |
|---|---|---|
| 筛选依据 | 按排名切 | 按概率累加和切 |
| 候选池大小 | 固定(永远是 K 个) | 动态(可能是 1 个,也可能是 500 个) |
| 极端场景表现 | 确定性下留太多废词,发散性下砍太多好词 | 确定性下自动收窄,发散性下自动放宽 |
| 工业界首选 | 较少单独用,常作为辅助 | 默认首选,因为它自适应 |
为什么工业界更爱 Top-P?
因为 Top-P 解决了 Top-K 最大的硬伤------ "K 值到底设多少?"
Top-K 的 K=50,可能今天写代码很好,明天写故事就太死板,后天做翻译又太发散。工程师得反复调。
Top-P 的 P=0.9,这个阈值有明确的物理意义:"我允许模型考虑 90% 的合理可能性"。无论什么场景,0.9 都是一个稳健的起点。
面试官追问:那它们是怎么配合的?
实际生产中不是二选一,而是组合拳:先调温度 → 再 Top-P → 最后 Top-K 兜底。
为什么要加 Top-K 兜底?因为有时候 Top-P 算出来的候选池可能有 5000 个词(概率极度分散时),会拖慢计算速度。加一个 Top-K 作为上限,比如
K=100------"最多只留 100 个,哪怕 Top-P 算出来要留 500 个,我也只取前 100 个"。
Q2:temperature 和 topK 的本质区别是什么?它们是如何共同影响生成的?
回答要点:
temperature作用于整个概率分布的平滑度,通过缩放 logits 改变所有候选词的概率比值。topK作用于候选集的大小,通过截断只保留概率最高的 K 个词。- 两者配合:先
topK截断(工程上更高效),再temperature重塑分布。temperature决定"怎么选",topK决定"从哪些里面选"。
面试官追问:单独用 Temperature 有什么问题?
无论温度调多低,所有词的概率永远不等于零。生成 1000 字长文时,低概率词的总累积概率依然可观,模型会在某个瞬间"抽风"蹦出乱码。只调温度相当于把噪音调小,但噪音永远在喇叭里。
面试官追问:单独用 Top-K 有什么问题?
Top-K 是一刀切的物理切割。在
1+1=的确定场景下,硬塞 50 个候选词增加了抽到错误答案的概率;在写诗的发散场景下,强行只留 50 个词,把剩下的好词全砍掉了。它没有"审美"能力,不知道语境是该"收窄"还是"放宽"。
Q3:LangChain 中的 pipe() 方法背后是什么设计模式?
回答要点:
- 核心是责任链模式(Chain of Responsibility) 与管道模式(Pipeline Pattern) 的结合。
- 每个节点(Runnable)都实现相同的接口
invoke()/stream(),通过pipe()串联成链。 - 数据在链中依次流过每个节点,每个节点可以转换、过滤、增强 数据,也可以分支、合并 (通过
RunnableBranch、RunnableParallel)。
Q4:如果我要在 Chain 中同时调用多个模型并汇总结果,LangChain 怎么支持?
回答要点:
- 使用
RunnableParallel(即prompt.pipe({ creative: creativeModel, precise: preciseModel }))。 - 并行执行两个模型调用,结果以对象形式合并输出,最后再
pipe一个 Parser 或聚合函数。 - 这是 LangChain 对扇出(Fan-out)模式的内置支持。