第二个 AI 调用——invoke 阻塞式与 stream 流式生成

接上篇《第一个 AI 调用------TypeScript 工程初始化与配置管理》,本篇在已经能成功调用大模型的基础上,深入对比 LangChain.js 里两种最常用的调用方式:invoke(阻塞式) 和 stream(流式)。读完你就能根据场景选对姿势,并避开几个真实的坑。

一、回顾:模型是怎么初始化的

无论用哪种方式,第一步都是创建一个 ChatOpenAI 实例(以 DeepSeek 为例):

ts 复制代码
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";

// 从 .env 读取配置:NANA_MODEL / NANA_API_KEY / NANA_API_BASE_URL
const getEnv = (key: string) => process.env[key] || "";

const llm = new ChatOpenAI({
  model: getEnv("NANA_MODEL"),            // 例如 deepseek-chat
  apiKey: getEnv("NANA_API_KEY"),        // sk-xxx
  configuration: {
    baseURL: getEnv("NANA_API_BASE_URL"), // https://api.deepseek.com/v1
  },
});

.env 关键配置(注意 baseURL 只写到 /v1,SDK 会自动拼接 /chat/completions):

env 复制代码
NANA_API_KEY=sk-xxxx
NANA_MODEL=deepseek-chat
NANA_API_BASE_URL=https://api.deepseek.com/v1

环境变量、依赖安装、Prettier 保存格式化等前置步骤见上篇,本文不再展开。

二、invoke:阻塞式调用

invoke 是最直观的调用方式------把整个 Prompt 丢进去,等模型把全部内容生成完,一次性返回结果。

ts 复制代码
const invoke = async (prompt: string) => {
  const res = await llm.invoke(prompt);
  console.log(res.content);
};

invoke("写一首关于AI的诗");

发生了什么

  1. 请求发出,客户端进入等待。
  2. 模型在服务器侧逐 token 生成,但服务端会攒齐全部 token 才一次性返回 HTTP 响应。
  3. await 拿到完整结果,res.content 就是诗歌全文(字符串)。

适用场景

  • 简短问答、翻译、摘要
  • 需要把结果当变量继续做逻辑处理(比如解析 JSON、喂给下一步)
  • 不关心"边生成边显示"的交互体验

缺点

  • 用户要干等整段生成完成。如果模型要写 500 字,可能要等好几秒才"啪"一下全部出现,体验差。
  • 长时间无输出,容易让人以为程序卡死了。

三、stream:流式调用

stream 让模型边生成边返回,前端/终端可以像打字机一样逐字吐出。这才是 ChatGPT 那种"逐字蹦字"体验的底层原理。

ts 复制代码
import { HumanMessage } from "@langchain/core/messages";

const stream = async () => {
  const stream = await llm.stream([new HumanMessage("写一首关于AI的诗")]);
  for await (const chunk of stream) {
    const content = Array.isArray(chunk.content)
      ? chunk.content.map((item) => ("text" in item ? item.text : "")).join("")
      : chunk.content;
    if (content) process.stdout.write(content);
  }
};

stream();

发生了什么

  1. 请求发出,服务端以 SSE(Server-Sent Events) 流的形式,每生成一小块 token 就推一段。
  2. llm.stream(...) 返回一个异步可迭代对象(AsyncIterable)。
  3. 用 for await...of 逐块消费,chunk.content 就是这一小段文本。
  4. process.stdout.write(content) 把碎片直接写到终端,不换行、不缓冲。

为什么要用 HumanMessage 而不是字符串

stream 接收的是 消息数组 ,不是裸字符串(裸字符串只有 invoke 支持)。标准写法是包一层 HumanMessage:

ts 复制代码
import { HumanMessage } from "@langchain/core/messages";

llm.stream([new HumanMessage("写一首关于AI的诗")]);

如果漏掉 import { HumanMessage },TS 会直接报"找不到名称 HumanMessage"。

chunk.content 为什么要做数组判断

LangChain 的 content 类型其实是 string | Array<...>------当返回纯文本时是字符串;当涉及多模态(图片、工具调用等)时会变成内容块数组。所以用 Array.isArray 兜底,保证只取文本部分:

ts 复制代码
const content = Array.isArray(chunk.content)
  ? chunk.content.map((item) => ("text" in item ? item.text : "")).join("")
  : chunk.content;

process.stdout.write vs console.log

  • console.log(content) 每次都会自动加换行 \n,一首诗会被切成一堆带空行的小段,很难看。
  • process.stdout.write(content) 原样输出、不换行,拼起来才是完整连贯的文本。

适用场景

  • 聊天对话、长文生成(体验最佳)
  • 任何需要"即时反馈"的交互
  • 前端打字机效果、Token 用量实时统计

四、两种方式对比

维度 invoke(阻塞式) stream(流式)
返回时机 全部生成完才返回 边生成边返回
返回类型 完整 AIMessage AsyncIterable<chunk>
消费方式 await 一次拿结果 for await...of 遍历
入参 字符串或消息数组 消息数组(推荐 HumanMessage)
终端体验 干等后整段出现 逐字打印,像打字机
取文本 res.content chunk.content(需遍历拼接)
适合场景 工具调用、JSON 解析、短答 对话、长文、实时展示
首次出字延迟 高(等整段) 低(首个 token 即出)

一句话总结:要拿结果做后续处理用 invoke,要给用户看的用 stream。

五、今天踩过的真实坑

  1. res.content 不是 res.concat 新手容易把字符串的 .concat() 方法当成响应属性,写出来 res.concat 得到 undefined。响应正文是 res.content。

  2. stream 入参要用消息对象 llm.stream([["human", "..."]]) 这种元组写法在新版 LangChain 已不支持,会抛 Unable to coerce message from array。改成 llm.stream([new HumanMessage("...")]) 或 llm.stream("...")。

  3. HumanMessage 必须导入 只写 new HumanMessage(...) 不 import,TS 直接报错"找不到名称"。

  4. .env 用 = 不用 : 写成 NANA_API_KEY:sk-xxx 会导致 dotenv 读不到,apiKey 变空字符串,报 Missing credentials。

  5. baseURL 别带 /chat/completions 写成 https://api.deepseek.com/v1/chat/completions,SDK 会再拼一次,最终请求变成 .../chat/completions/chat/completions,直接 404。只写到 https://api.deepseek.com/v1。

六、运行验证

bash 复制代码
# 编译
pnpm build
# 运行(默认走 stream)
node dist/index.js

终端会逐字打印出 DeepSeek 写的诗,证明流式调用已通。

想对比 invoke,把被注释的那段解开、注释掉 stream 即可:

ts 复制代码
const res = await llm.invoke("写一首关于AI的诗");
console.log(res.content);

七、小结

  • invoke 是"一次性拿结果",适合程序内部消费;stream 是"边生成边拿",适合给用户看。
  • 流式关键是 AsyncIterable + for await...of + process.stdout.write。
  • content 可能是数组,生产代码要做类型兜底。
  • 所有配置(key、baseURL)都来自 .env,改配置不用重新编译,重启进程即可。

下一篇可以聊 多轮对话(带 history 的 messages 数组) 或 RAG 向量检索,把"单次问答"升级成"能记住上下文的助手"。


相关代码 :nana-ima/1.basic/src/index.ts 环境搭建:见《第一个 AI 调用------TypeScript 工程初始化与配置管理》

相关推荐
阿里云大数据AI技术几秒前
阿里云 Milvus 磁盘索引新利器:百万级Topk高性能召回
人工智能
独立开发者阿乐几秒前
llms.txt 是什么?为什么 AI 爬虫需要这个文件
人工智能·爬虫·geo优化·ai爬虫·llms.txt·ai收录·robots.txt
福兮说30 分钟前
秋招简历让 AI 写了一版:它替我加了 JWT,还把课程设计写成了「高并发」
人工智能·课程设计·秋招·求职·简历
ZDN_is_beauty33 分钟前
深度学习2-卷积神经网络
人工智能·深度学习·cnn
QQ机器人_插件39 分钟前
鲲鹏恒控的跨平台交付 —— Windows 上组态,Linux 上运行
人工智能
XMAIPC_Robot40 分钟前
储能控制器产品化:ARM+FPGA整机高低温、EMC可靠性测试实战总结
arm开发·人工智能·fpga开发·arm+fpga·rk3588+fpga
Martina_032143 分钟前
AI生成的海岸城市一移动镜头,水面反射就跳?用6步统一反射探针、SSR与平面反射
图像处理·人工智能·平面·3d·ai·自然语言处理·游戏策划
Matlab程序猿1 小时前
【MATLAB源码-第459期】基于matlab的D-H模型与阻尼最小二乘逆运动学的六自由度机械臂五次轨迹规划及动力学前馈PD控制仿真。
人工智能·算法·matlab·d-h建模·六自由度串联机械臂
QYR-分析1 小时前
人机交互升级浪潮下,全球电容式触摸传感器市场格局与细分赛道深度研判
人工智能·人机交互
血小溅1 小时前
什么是 Agent?
人工智能