📖 本章学习目标
- ✅ 配置完整的 LangChain.js 开发环境(Node.js + TypeScript)
- ✅ 理解模块化包安装的设计理念和最佳实践
- ✅ 创建并运行第一个可工作的 AI Agent
- ✅ 接入 LangSmith 实现可视化调试
- ✅ 掌握流式输出的实现方法
- ✅ 识别并避免常见的配置陷阱
一、开发环境准备
在写第一行代码之前,先把开发工具配好。这就像厨师做菜前要准备好厨具和食材一样重要。
1、Node.js 与包管理器
LangChain.js 要求 Node.js 20+(推荐 LTS 版本)。为什么需要这么高的版本?因为 LangChain.js 大量使用了现代 JavaScript 特性(如 ES Modules、Top-level await),这些特性在旧版本中支持不完善。
(1)检查当前Node版本
bash
node -v # 应该输出 v20.x.x 或更高
如果版本过低,需要升级。不同操作系统有不同的管理工具:
macOS / Linux 用户: 推荐使用 nvm(Node Version Manager)
bash
# 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# 重新加载 shell 配置
source ~/.bashrc # 或 source ~/.zshrc
# 安装并使用 Node.js 20
nvm install 20
nvm use 20
Windows 用户: 推荐使用 fnm(Fast Node Manager),比 nvm for Windows 更稳定
bash
# 使用 winget 安装 fnm
winget install Schniz.fnm
# 安装并使用 Node.js 20
fnm install 20
fnm use 20
(2)安装包管理器
包管理器推荐使用 pnpm,相比 npm 和 yarn,它有两大优势:
- 更快:使用硬链接和符号链接,避免重复下载
- 更节省磁盘空间:全局存储依赖,多个项目共享
bash
# 通过 npm 安装 pnpm(全局安装)
npm install -g pnpm
# 验证安装成功
pnpm -v
💡 小贴士:如果你已经熟悉 npm 或 yarn,继续使用也完全没问题。本系列教程的命令会用 pnpm,你可以轻松转换为对应的 npm/yarn 命令。
2、TypeScript 配置
所有教程的代码使用 TypeScript,原因有三:
- 类型安全:在编码阶段就能发现很多错误
- IDE 支持:智能提示和自动补全更准确
- 生产实践:大型项目几乎都用 TypeScript
(3)初始化项目
bash
# 创建项目目录并进入
mkdir my-langchain-agent && cd my-langchain-agent
# 初始化 package.json
pnpm init
# 安装 TypeScript 相关依赖(-D 表示开发依赖)
pnpm add -D typescript tsx @types/node
依赖说明:
typescript:TypeScript 编译器tsx:直接运行 TypeScript 文件的工具(无需手动编译)@types/node:Node.js 的类型定义
(4)创建 TypeScript 配置文件
在项目根目录创建 tsconfig.json:
json
{
"compilerOptions": {
"target": "ES2022", // 目标 JavaScript 版本
"module": "ESNext", // 模块系统
"moduleResolution": "bundler", // 模块解析策略
"strict": true, // 启用严格类型检查
"esModuleInterop": true, // 允许 import CommonJS 模块
"skipLibCheck": true, // 跳过库文件的类型检查(加速编译)
"outDir": "./dist", // 编译输出目录
"rootDir": "./src" // 源代码目录
},
"include": ["src/**/*"] // 包含的文件
}
关键配置解读:
"strict": true:开启所有严格类型检查选项,帮助你在编码阶段发现潜在问题"moduleResolution": "bundler":现代化的模块解析策略,更适合打包工具"esModuleInterop": true:让你可以用import x from 'module'的方式导入 CommonJS 模块
(5)配置运行脚本
在 package.json 中添加脚本,方便开发和构建:
json
{
"scripts": {
"dev": "tsx src/index.ts", // 开发模式:直接运行 TS 文件
"build": "tsc" // 构建模式:编译为 JS
},
"type": "module" // ⚠️ 重要:声明使用 ESM 模块系统
}
⚠️ 注意 :
"type": "module"这一行非常重要!它告诉 Node.js 你的项目使用 ES Modules(而不是传统的 CommonJS)。缺少这一行会导致import语句报错。
3、安装 LangChain.js
LangChain.js 采用模块化设计,核心包和各个 Provider 集成包分开安装。
(1)为什么要模块化?
想象一下,如果你只需要 OpenAI 的模型,却要安装 Anthropic、Google、Ollama 等所有 Provider 的依赖,会造成:
- 安装包体积巨大(可能超过 100MB)
- 安装时间长
- 潜在的依赖冲突
模块化设计让你只安装需要的部分。
(2)安装核心包
bash
# LangChain 核心包(必须)
pnpm add langchain
(3) 安装 Provider 集成包
根据你的需求选择安装(至少选一个):
bash
# OpenAI(GPT-4o、GPT-4o-mini 等)
pnpm add @langchain/openai
# Anthropic(Claude Opus、Sonnet、Haiku 等)
pnpm add @langchain/anthropic
# Google(Gemini 系列)
pnpm add @langchain/google-genai
# Ollama(本地运行的开源模型)
pnpm add @langchain/ollama
(5)安装 LangGraph
bash
# LangGraph(底层运行时,Agent 的基础)
pnpm add @langchain/langgraph
💡 提示:虽然 LangGraph 是底层依赖,但建议显式安装,确保版本可控。
4. 安装类型校验库Zod
Zod 是一个 TypeScript 类型验证库,用于确保输入数据符合定义的格式。
bash
pnpm add zod
5、配置 API Key
大多数 LLM Provider 都需要 API Key 才能使用。我们需要安全地存储这些密钥。
(1)创建环境变量文件
在项目根目录创建 .env 文件:
bash
# .env 文件(存储敏感信息,不要提交到 Git)
# OpenAI API Key(从 https://platform.openai.com/api-keys 获取)
OPENAI_API_KEY=sk-your-openai-key-here
# Anthropic API Key(可选,如果使用 Claude 模型)
ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here
# LangSmith 配置(可选,但强烈推荐用于调试)
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=ls-your-langsmith-key-here
LANGSMITH_PROJECT=my-first-agent
(2)创建 .gitignore 文件
为了防止敏感信息泄露,必须把 .env 加入 .gitignore:
bash
# .gitignore
.env
node_modules/
dist/
(3)安装 dotenv 包
dotenv 是一个轻量级的库,用于在应用启动时加载 .env 文件中的环境变量:
bash
pnpm add dotenv
最终的项目结构
bash
my-langchain-agent/
├── src/
│ └── index.ts # 主入口文件
├── .env # API Keys(不提交到 Git)
├── .gitignore # Git 忽略配置
├── package.json # 项目配置和依赖
└── tsconfig.json # TypeScript 配置
二、第一个 Agent:天气查询示例
环境配好了,现在来写第一个能真正运行的 Agent。我们从一个简单的场景开始:查询天气。
1、最简版本:20 行代码
这个示例展示如何用最少代码创建一个可用的 Agent。
(1)完整代码
ts
// src/index.ts
// 第一步:加载环境变量(必须在最顶部)
import "dotenv/config";
// 第二步:导入 LangChain 核心功能
import { createAgent, tool } from "langchain";
import { z } from "zod";
// 第三步:定义天气查询工具
const getWeather = tool(
({ city }) => `${city} 今日天气:晴,气温 22°C,湿度 60%`,
{
name: "get_weather",
description: "查询指定城市的当前天气",
schema: z.object({
city: z.string().describe("要查询天气的城市名称"),
}),
}
);
// 第四步:创建 Agent
const agent = createAgent({
model: "openai:gpt-4o",
tools: [getWeather],
});
// 第五步:调用 Agent 并输出结果
const result = await agent.invoke({
messages: [{ role: "user", content: "北京和上海今天天气怎么样?" }],
});
console.log(result.messages.at(-1)?.content);
(2)运行代码
bash
pnpm dev
(3)预期输出
bash
北京今日天气:晴,气温 22°C,湿度 60%
上海今日天气:晴,气温 22°C,湿度 60%
两个城市今天天气都不错,都是晴天,气温适中。如果你计划出行,不用担心雨天哦!
✨ 神奇之处:你只定义了一个查询单个城市天气的工具,但 Agent 自动判断需要查询两个城市,连续调用了两次工具,最后生成了汇总回答。这就是 Agent 的智能之处!
2、代码逐段解析
让我们深入理解每一部分的作用。
(1)导入依赖
ts
// 加载环境变量(让 process.env.OPENAI_API_KEY 可用)
import "dotenv/config";
// 导入 LangChain 核心功能
import { createAgent, tool } from "langchain";
// 导入 Zod(用于定义参数校验规则)
import { z } from "zod";
关键点:
dotenv/config必须在最顶部导入,确保在其他代码执行前加载环境变量createAgent:创建 Agent 的核心函数tool:将普通函数包装成 Agent 可调用的工具zod:类型安全的参数校验库(LangChain.js 内置依赖)
(2)定义工具
ts
const getWeather = tool(
// 参数 1:工具的执行函数
// 接收解构的参数对象,返回字符串结果
({ city }) => `${city} 今日天气:晴,气温 22°C,湿度 60%`,
// 参数 2:工具的元数据(告诉 LLM 如何使用这个工具)
{
name: "get_weather", // 工具的唯一标识符
description: "查询指定城市的当前天气", // 工具的功能描述
schema: z.object({ // 参数校验规则
city: z.string().describe("要查询天气的城市名称"),
}),
}
);
工具定义的三个关键要素:
| 要素 | 作用 | 重要性 |
|---|---|---|
name |
LLM 用来引用这个工具 | ⭐⭐⭐ 必须唯一 |
description |
LLM 根据描述决定是否调用 | ⭐⭐⭐⭐⭐ 最关键 |
schema |
定义参数的类型和约束 | ⭐⭐⭐⭐ 保证类型安全 |
💡 最佳实践 :
description写得越详细(明确而不是字多),Agent 的表现越好。例如:
typescriptdescription: "查询指定城市的当前天气情况,包括温度、湿度、天气状况。当用户询问某个地方的天气时使用此工具。"
(3)创建 Agent
ts
const agent = createAgent({
model: "openai:gpt-4o", // 使用的模型(Provider:模型名格式)
tools: [getWeather], // 注册给 Agent 的工具列表
});
支持的模型格式:
bash
// OpenAI 系列
model: "openai:gpt-4o"
model: "openai:gpt-4o-mini"
model: "openai:o1-mini"
// Anthropic 系列
model: "anthropic:claude-opus-4-5"
model: "anthropic:claude-sonnet-4-6"
// Google 系列
model: "google:gemini-2.0-flash"
(4)调用 Agent
ts
const result = await agent.invoke({
messages: [{ role: "user", content: "北京和上海今天天气怎么样?" }],
});
返回值结构:
js
result = {
messages: [
{ role: "user", content: "北京和上海今天天气怎么样?" },
{ role: "assistant", content: "", tool_calls: [...] }, // LLM 决定调用工具
{ role: "tool", content: "北京 今日天气:晴...", name: "get_weather" },
{ role: "assistant", content: "", tool_calls: [...] }, // 再次调用工具
{ role: "tool", content: "上海 今日天气:晴...", name: "get_weather" },
{ role: "assistant", content: "两个城市今天天气都不错..." } // 最终回答
]
}
使用 result.messages.at(-1)?.content 获取最后一条消息(即最终回答)。
3、Agent 执行流程可视化
为了帮助你理解 Agent 的内部工作流程,我们用序列图展示:
决定需要查天气 L-->>A: tool_call(get_weather, {city: "北京"}) A->>T: 执行工具调用 T-->>A: "北京 今日天气:晴..." A->>L: 传入工具返回结果 Note over L: LLM 发现还需要查上海 L-->>A: tool_call(get_weather, {city: "上海"}) A->>T: 执行工具调用 T-->>A: "上海 今日天气:晴..." A->>L: 传入全部结果 Note over L: LLM 生成汇总回答 L-->>A: "两个城市今天天气都不错..." A-->>U: 返回最终回答
关键点:
- Agent 会自主决策调用几次工具
- 每次工具调用后,结果都会回传给 LLM
- LLM 基于所有历史信息决定下一步行动
- 整个过程是循环迭代的,直到 LLM 认为任务完成
三、接入 LangSmith:让每一步都可见
上面的示例跑通了,但你有没有想过:
- Agent 在执行过程中,LLM 具体传了什么 Prompt?
- 工具调用的参数是什么?
- 每一步花了多少时间?
- 用了多少 Token,成本是多少?
在传统软件开发里,你可以加 console.log 来调试。但在 Agent 开发中,调用链路可能有十几步,而且 LLM 的每次调用都有延迟,手动打日志效率很低。
LangSmith 就是为这个场景设计的可观测性平台。
1、注册并获取 API Key
步骤 1:访问官网
前往 LangSmith 官网
步骤 2:注册账号
可以使用 GitHub 账号直接登录,或者用邮箱注册。
步骤 3:创建项目
登录后,点击 "Create Project",输入项目名称(如 my-first-agent)。
步骤 4:获取 API Key
在项目设置页面,找到 "API Keys" 标签页,复制你的 API Key。
2、开启追踪
在 .env 文件中添加以下配置:
bash
# .env
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=ls_你的API_Key
LANGSMITH_PROJECT=my-first-agent # 项目名,可以自定义
就这样!不需要修改任何业务代码。
3、查看执行追踪
再次运行你的 Agent:
bash
pnpm dev
然后打开 LangSmith 控制台(smith.langchain.com),你会看到类似这样的完整执行追踪:
bash
Run: "北京和上海今天天气怎么样?"
├── 🤖 LLM Call 1(决策阶段)
│ ├── Input: [system prompt] + [user message] + [工具定义]
│ ├── Output: tool_call(get_weather, {city: "北京"})
│ ├── Latency: 1.2s
│ └── Tokens: 312 (prompt: 280, completion: 32)
│
├── 🔧 Tool Call: get_weather
│ ├── Input: {city: "北京"}
│ └── Output: "北京 今日天气:晴..."
│
├── 🤖 LLM Call 2(再次决策)
│ ├── Input: [...历史消息] + [工具结果]
│ ├── Output: tool_call(get_weather, {city: "上海"})
│ ├── Latency: 0.9s
│ └── Tokens: 198
│
├── 🔧 Tool Call: get_weather
│ ├── Input: {city: "上海"}
│ └── Output: "上海 今日天气:晴..."
│
└── 🤖 LLM Call 3(生成最终回答)
├── Input: [...历史消息] + [所有工具结果]
├── Output: "两个城市今天天气都不错..."
├── Latency: 1.5s
└── Tokens: 267
你可以在 LangSmith 中看到:
- ✅ 每次 LLM 调用的完整输入输出
- ✅ 工具调用的参数和返回值
- ✅ 每一步的耗时和 Token 消耗
- ✅ 整个执行链路的可视化流程图
🔍 为什么可观测性在 AI 开发中尤为重要?
传统软件的 bug 往往是确定性的------同样的输入,永远产生同样的错误。LLM 应用则不同:
- 模型输出具有随机性(即使 temperature=0)
- 同一个 Prompt 在不同时刻可能给出不同的工具调用决策
- Agent 的行为依赖于多轮交互的累积状态
如果你不追踪每次执行的完整链路,遇到 Agent 行为异常时几乎无从下手。
建议:从第一天就开启 LangSmith,养成在 LangSmith 里调试的习惯,而不是只看终端输出。
四、流式输出:让 Agent 实时响应
上面的示例使用 invoke(),需要等 Agent 完成全部推理才返回结果。对于耗时较长的任务(比如需要调用多次工具),用户体验不好(用户只能盯着空白屏幕等待 5-10 秒甚至更长)。
流式输出(Streaming) 可以让 Agent 的中间过程和最终输出实时传输,实现类似 ChatGPT 的"打字机效果"。
1、基础流式示例
ts
// src/streaming.ts
import "dotenv/config";
import { createAgent, tool } from "langchain";
import { z } from "zod";
// 定义工具(和之前一样)
const getWeather = tool(
({ city }) => `${city} 今日天气:晴,气温 22°C,湿度 60%`,
{
name: "get_weather",
description: "查询指定城市的当前天气",
schema: z.object({ city: z.string() }),
}
);
// 创建 Agent(和之前一样)
const agent = createAgent({
model: "openai:gpt-4o",
tools: [getWeather],
});
// 使用 stream() 替代 invoke()
const stream = await agent.stream(
{ messages: [{ role: "user", content: "北京今天天气怎么样?" }] },
{ streamMode: "values" } // 流式返回每次状态更新
);
// 遍历流式数据
for await (const chunk of stream) {
const lastMessage = chunk.messages.at(-1);
if (lastMessage?.role === "assistant") {
process.stdout.write(lastMessage.content as string);
}
}
代码解读:
- 第 19 行:使用
stream()方法替代invoke() - 第 21 行:
streamMode: "values"表示返回每次状态更新的完整快照 - 第 24-28 行:使用
for await...of异步遍历流式数据 - 第 26-27 行:只输出
assistant角色的消息内容
2、streamMode 的三种模式
| 模式 | 返回内容 | 适用场景 |
|---|---|---|
"values" |
每次状态更新后的完整状态 | 需要展示中间步骤,了解 Agent 的思考过程 |
"updates" |
每次状态变化的增量部分 | 只关心变化部分,减少数据传输 |
"messages" |
逐 Token 流式输出 | 对话界面的打字机效果,用户体验最佳 |
3、逐 Token 流式输出(推荐用于聊天界面)
如果你想实现类似 ChatGPT 的效果(文字逐个出现),使用 "messages" 模式:
ts
const stream = await agent.stream(
{ messages: [{ role: "user", content: "北京今天天气怎么样?" }] },
{ streamMode: "messages" } // 逐 Token 流式输出
);
for await (const [message, metadata] of stream) {
if (message.role === "assistant") {
// 逐字符输出,实现打字机效果
process.stdout.write(message.content);
}
}
效果:
bash
北→京→今→日→天→气→:→晴→,→气→温→ →2→2→°→C...
💡 实际应用场景:
- Web 应用:使用
Server-Sent Events (SSE)或WebSocket将流式数据推送到前端- CLI 工具:直接在终端逐字输出,提升交互体验
- 移动端:分块渲染,减少首屏等待时间
五、完整的项目结构
至此,一个基本的 LangChain.js 项目结构如下:
bash
my-langchain-agent/
├── src/
│ ├── tools/ # 工具定义目录
│ │ └── weather.ts # 天气查询工具
│ ├── agents/ # Agent 配置目录
│ │ └── main.ts # 主 Agent 创建逻辑
│ └── index.ts # 入口文件
├── .env # API Keys(不提交 Git)
├── .env.example # 环境变量模板(提交 Git)
├── .gitignore # Git 忽略配置
├── package.json # 项目配置
└── tsconfig.json # TypeScript 配置
添加.env.example 文件
这是一个模板文件,提交到代码仓库,告诉其他开发者需要配置哪些环境变量:
bash
# .env.example(提交到 Git)
# OpenAI API Key(必填)
# 从 https://platform.openai.com/api-keys 获取
OPENAI_API_KEY=
# Anthropic API Key(可选)
# 从 https://console.anthropic.com/settings/keys 获取
ANTHROPIC_API_KEY=
# LangSmith 配置(推荐)
# 从 https://smith.langchain.com 获取
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=
LANGSMITH_PROJECT=my-first-agent
使用方法:
bash
# 克隆项目后,复制模板并填写真实值
cp .env.example .env
# 然后用编辑器打开 .env 填入真实的 API Key
六、常见问题与踩坑指南
在实际开发中,你可能会遇到以下问题。这里整理了最常见的几个坑和解决方案。
⚠️ 踩坑 1:Cannot use import statement in a module 错误
错误现象:
bash
SyntaxError: Cannot use import statement outside a module
原因: Node.js 默认使用 CommonJS 模块系统(require/module.exports),而 LangChain.js 使用 ES Modules(import/export)。
解决方案: 在 package.json 中添加 "type": "module"
json
{
"name": "my-langchain-agent",
"version": "1.0.0",
"type": "module", // ← 添加这一行
"scripts": {
"dev": "tsx src/index.ts"
}
}
⚠️ 踩坑 2:API Key 没有生效
错误现象:
bash
AuthenticationError: Invalid API key
原因: dotenv/config 需要在代码最顶部导入,且必须在其他 LangChain 导入之前。
解决方案:
ts
// ✅ 正确:dotenv 必须第一行
import "dotenv/config";
import { createAgent } from "langchain";
// ❌ 错误:dotenv 在后面,环境变量可能已经被读取过
import { createAgent } from "langchain";
import "dotenv/config";
验证方法:
typescript
import "dotenv/config";
console.log("API Key:", process.env.OPENAI_API_KEY?.substring(0, 10) + "...");
// 应该输出:API Key: sk-xxxxx...
⚠️ 踩坑 3:工具返回值类型错误
错误现象:
bash
TypeError: Tool output must be a string
原因: 工具函数的返回值必须是字符串(或者能序列化为字符串的内容)。
解决方案: 如果工具返回的是对象或数组,手动序列化:
ts
const searchTool = tool(
async ({ query }) => {
const results = await fetchSearchResults(query);
// ❌ 错误:返回对象
// return results;
// ✅ 正确:序列化为 JSON 字符串
return JSON.stringify(results);
},
{
name: "search",
description: "搜索互联网",
schema: z.object({ query: z.string() })
}
);
⚠️ 踩坑 4:model 字符串格式错误
错误现象:
bash
Error: Invalid model identifier
原因: v1.x 的 model 参数使用 "provider:model-name" 格式,而不是直接传模型实例。
解决方案:
ts
// ✅ 正确:使用字符串标识符(推荐)
createAgent({ model: "openai:gpt-4o" })
// ✅ 也可以:显式实例化(需要精细配置时)
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0.7 });
createAgent({ model })
// ❌ 错误:v0.x 的旧写法(已废弃)
createAgent({ llm: new ChatOpenAI(...) })
⚠️ 踩坑 5:Zod Schema 定义不完整
错误现象: Agent 调用工具时传入了错误的参数类型,但没有报错。
原因: Zod Schema 定义不够严格,缺少必要的校验规则。
解决方案: 为每个参数添加详细的描述和校验:
ts
// ❌ 不够严格
schema: z.object({
city: z.string(),
})
// ✅ 更严格的定义
schema: z.object({
city: z.string()
.min(1, "城市名称不能为空")
.max(50, "城市名称过长")
.describe("要查询天气的城市名称,如:北京、上海、广州"),
})
七、本章小结
恭喜你完成了第一章的实战练习!这一章我们完成了三件重要的事情:
✅ 学习成果回顾
-
搭好了开发环境
- Node.js 20+ 和 pnpm 包管理器
- TypeScript 配置和 tsx 运行工具
- LangChain.js 模块化安装策略
-
跑通了第一个 Agent
- 理解了
tool()定义工具的三要素(name、description、schema) - 掌握了
createAgent()创建 Agent 的方法 - 学会了
invoke()同步调用和stream()流式输出
- 理解了
-
接入了 LangSmith
- 用两个环境变量就开启了完整的执行链路追踪
- 理解了可观测性在 AI 开发中的重要性
🎯 动手练习
尝试完成以下练习,巩固所学知识:
练习 1:扩展天气工具 修改 getWeather 工具,让它返回更真实的天气数据(可以调用免费的天气 API,如 OpenWeatherMap)。
练习 2:添加新工具 创建一个 getTime 工具,返回当前时间。测试 Agent 能否正确回答"现在几点了?"
练习 3:对比不同模型 将模型从 "openai:gpt-4o" 改为 "openai:gpt-4o-mini",观察响应速度和质量的差异。
练习 4:探索 LangSmith 在 LangSmith 控制台中查看你的 Agent 执行轨迹,找出:
- 总共调用了几次 LLM?
- 每次调用的 Token 消耗是多少?
- 工具调用的参数是否正确?
📚 延伸阅读
下一章:第三章 ------ 模型抽象层(Models & Messages)