AI 电子伴侣
创建时间: 2026-03-29最后更新: 2026-08-04

1. 先把调用跑通

这一篇不追求讲很多概念,只做一件事:先把 LangChain 跑起来

我用的是 DeepSeek,但接法走的是 OpenAI 兼容接口,所以代码里会用到 @langchain/openaiChatOpenAI

我们会按三个小步骤往下走:

  1. 先直接调一次模型
  2. 再试一次流式输出
  3. 最后再换成一个最小 Agent

这样走下来,后面再看消息、Prompt、Tool、Agent,就不会觉得突然

2. 先看目录结构

第一次尝试 LangChain,建议不要一上来就塞进页面里。
先单独放一个 playground,更容易定位问题。

当前目录结构是这样的:

.env.localDeepSeek 的本地环境变量
.gitignore忽略密钥和依赖
package.json依赖和运行命令
tsconfig.jsonTypeScript 配置
first-call.ts第一次完整调用
first-stream.ts第一次流式输出
first-agent.ts第一次 Agent 流式调用

3. 依赖和命令

先看当前 playground 里的 package.json

package.json
01
{
02
"name": "langchain-first-call-playground",
03
"private": true,
04
"type": "module",
05
"packageManager": "yarn@4.12.0",
06
"scripts": {
07
"first-call": "tsx scripts/first-call.ts",
08
"first-stream": "tsx scripts/first-stream.ts",
09
"first-agent": "tsx scripts/first-agent.ts"
10
},
11
"dependencies": {
12
"@langchain/core": "^1.1.36",
13
"@langchain/openai": "^1.3.1",
14
"dotenv": "^17.3.1",
15
"langchain": "^1.2.37"
16
},
17
"devDependencies": {
18
"tsx": "^4.21.0",
19
"typescript": "^6.0.2"
20
}
21
}

这里先记住几个最常用的:

  • @langchain/openai:负责接 OpenAI 兼容接口
  • langchain:后面写 Agent 会用到
  • dotenv:读取 .env.local
  • tsx:直接运行 TypeScript 脚本

4. 先把环境变量配对

当前这套 playground 用的是下面这组三个变量:

.env.local
1
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxx
2
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
3
DEEPSEEK_MODEL=deepseek-chat

这三个字段分别对应:

  • DEEPSEEK_API_KEY:密钥
  • DEEPSEEK_BASE_URL:OpenAI 兼容接口地址
  • DEEPSEEK_MODEL:模型名

这里有个很容易漏掉的细节:

DEEPSEEK_BASE_URL 要带上 /v1
如果少了这一段,脚本很容易直接报错。

5. 第一次完整调用:first-call.ts

第一步先不要碰 Agent,直接调模型。

scripts/first-call.ts
01
import dotenv from 'dotenv'
02
import { ChatOpenAI } from '@langchain/openai'
03
04
dotenv.config({ path: new URL('../.env.local', import.meta.url) })
05
06
const model = new ChatOpenAI({
07
apiKey: process.env.DEEPSEEK_API_KEY,
08
model: process.env.DEEPSEEK_MODEL ?? 'deepseek-chat',
09
configuration: {
10
baseURL: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com/v1',
11
},
12
})
13
14
const response = await model.invoke([
15
{
16
role: 'system',
17
content: '你是一名面向前端开发者的助手,回答要清楚、简短。',
18
},
19
{
20
role: 'user',
21
content: '请用两句话确认 LangChain 与 DeepSeek 的连接已经正常。',
22
},
23
])
24
25
console.log('invoke result:')
26
console.log(response.text)

执行命令:

code.ts
1
yarn first-call

这一段最值得记住的是 invoke() 的感觉:

  • 把一份完整输入交给模型
  • 等模型生成结束
  • 一次性拿回结果

6. 第二次:换成流式输出

scripts/first-stream.ts
01
import dotenv from 'dotenv'
02
import { ChatOpenAI } from '@langchain/openai'
03
04
dotenv.config({ path: new URL('../.env.local', import.meta.url) })
05
06
const model = new ChatOpenAI({
07
apiKey: process.env.DEEPSEEK_API_KEY,
08
model: process.env.DEEPSEEK_MODEL ?? 'deepseek-chat',
09
configuration: {
10
baseURL: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com/v1',
11
},
12
})
13
14
const stream = await model.stream([
15
{
16
role: 'system',
17
content: '你是一名面向前端开发者的助手,回答要自然、简短。',
18
},
19
{
20
role: 'user',
21
content: '请用一句话说明当前是流式输出验证。',
22
},
23
])
24
25
process.stdout.write('stream result:\n')
26
27
for await (const chunk of stream) {
28
process.stdout.write(chunk.text)
29
}
30
31
process.stdout.write('\n')

执行命令:

code.ts
1
cd apps/aicompanion/playgrounds/langchain-first-call
2
yarn first-stream

这时候最大的变化只有一个:
不再等完整结果,而是边生成边输出。

所以你可以先把区别简单记成这样:

  • invoke():一次性拿结果
  • stream():边生成边拿结果

7. 第三次:换成最小 Agent

前面两段代码都还是“直接调模型”。

现在再往前走一步,看看最小 Agent 是什么样。

scripts/first-agent.ts
01
import dotenv from 'dotenv'
02
import { createAgent } from 'langchain'
03
import { ChatOpenAI } from '@langchain/openai'
04
05
dotenv.config({ path: new URL('../.env.local', import.meta.url) })
06
07
// 定义模型
08
const model = new ChatOpenAI({
09
apiKey: process.env.DEEPSEEK_API_KEY,
10
model: process.env.DEEPSEEK_MODEL ?? 'deepseek-chat',
11
configuration: {
12
baseURL: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com/v1',
13
},
14
})
15
16
// 定义 Agent
17
const agent = createAgent({
18
model,
19
tools: [],
20
systemPrompt: '你是一名面向前端开发者的助手,回答要自然、简短。',
21
})
22
23
// 定义 message
24
const inputMessages = {
25
role: 'user',
26
content: '请用一句话说明当前是 Agent 流式调用验证。',
27
}
28
29
// Agent 调用 stream 流式输出,返回的是消息流
30
const stream = await agent.stream({
31
messages: [inputMessages],
32
}, {
33
// 设置 streamMode 为 messages,返回的是消息流
34
streamMode: 'messages',
35
})
36
37
process.stdout.write('agent stream result:\n')
38
39
for await (const [messageChunk] of stream) {
40
if (messageChunk.content) {
41
process.stdout.write(messageChunk.text)
42
}
43
}
44
45
process.stdout.write('\n')

执行命令:

code.ts
1
yarn first-agent

这段代码里最值得注意的地方有三个。

第一,模型配置本身没有变。
也就是说,Agent 不是另一套模型初始化方式,它还是建立在同一个模型对象之上。

第二,真正变化的是入口。
前面是:

code.ts
1
model.invoke(...)
2
model.stream(...)

这里变成了:

code.ts
1
agent.stream(...)

第三,createAgent() 让模型外面多了一层运行时包装。
现在这个例子里还没有工具,所以它看上去像是“绕了一层再调模型”。但后面一旦把 tools 接进去,这层包装的价值就会很明显。

8. 这个 Agent 版本里,多出来了什么

如果你第一次看 first-agent.ts,最容易疑惑的是:
“它和 first-stream.ts 看起来差不多,为什么还要单独写 Agent 版?”

原因就在于,后面我们整章要讲的主线不是“怎么调一个模型”,而是:

单个 Agent 怎样在一轮请求里调用多个工具,把事情做完。

所以这里先放一个最小 Agent,有两个作用:

  • 先把 createAgent()agent.stream() 这些入口认熟
  • 后面加工具时,不需要再突然切换思路

换句话说,first-call.tsfirst-stream.ts 是在确认底层模型调用正常,first-agent.ts 则是在给后面的 Agent 主线铺路。

9. 总结

第一次不要试图把所有细节都吃透,先看懂下面四件事就够了。

9.1 dotenv.config({ path: new URL(...) })

这里显式指定了 .env.local 的位置。

这样做的好处是:只要脚本文件路径不变,就能稳定找到同一个环境变量文件,不容易因为执行目录变化而读错环境变量。

9.2 消息是数组,不是单个字符串

无论是 model.invoke()model.stream(),还是 agent.stream(),这里传进去的都不是单一字符串,而是一组消息。

这会比“拼一整段字符串”更适合后面的多轮对话和工具调用场景。

9.3 response.textchunk.text

在直接调模型时:

  • 完整返回看 response.text
  • 流式返回看 chunk.text

第一次跑通时,先这么理解最省事。

9.4 streamMode: 'messages'

first-agent.ts 里,这一项很关键:

code.ts
1
{
2
streamMode: 'messages'
3
}

这样拿到的是消息流,终端里可以直接边生成边打印文字。
如果没有这层设置,Agent 的流式返回会更偏运行时事件结构,不适合做这篇的最小示例