详情

首页手游攻略 从零开始搭建一个 AI Agent - LangChain + TypeScript 实战手记

从零开始搭建一个 AI Agent - LangChain + TypeScript 实战手记

佚名 2026-07-01 09:34:51

从零开始搭建一个 AI Agent —— LangChain + TypeScript 实战手记

目录

  1. 项目初始化:先把骨架搭起来
  2. 理解 Agent:它和普通 LLM 调用有什么不同?
  3. 定义工具:让 AI 拥有"手"
  4. 创建 Agent:把大脑和手连起来
  5. 流式输出:别让用户干等着
  6. 多轮对话:让 Agent 记住上下文
  7. Extended Thinking:看见模型的思考过程
  8. 回顾与展望

1. 项目初始化:先把骨架搭起来

1.1 创建项目

mkdir lingshi && cd lingshipnpm init

这一步没什么特别的,得到一个 package.json,后面往里加依赖就行。

从零开始搭建一个 AI Agent —— LangChain + TypeScript 实战手记

1.2 安装依赖

# 运行时依赖pnpm add langchain @langchain/anthropic @langchain/core @langchain/langgraph deepagents zod dotenv# 开发依赖(TypeScript 相关)pnpm add -D typescript tsx @types/node

简单解释一下每个包的职责:

包名作用
langchainLangChain 核心框架
@langchain/anthropicAnthropic 兼容接口的 ChatModel
@langchain/core核心工具定义(tool 函数从这里来)
@langchain/langgraphAgent 的图结构引擎 + MemorySaver
deepagents封装了 createDeepAgent,简化 Agent 创建
zod运行时类型校验,用来定义工具的参数 schema
dotenv加载 .env 文件中的环境变量
tsx直接运行 TypeScript,不需要先编译

1.3 配置 TypeScript

创建 tsconfig.json

{"compilerOptions": {"target": "ES2022","module": "ESNext","moduleResolution": "bundler","esModuleInterop": true,"strict": true,"skipLibCheck": true,"noEmit": true,"types": ["node"]},"include": ["src/**/*"]}

几个关键配置:

  • module: "ESNext" — 使用 ES 模块(import/export),对应 package.json 中的 "type": "module"
  • moduleResolution: "bundler" — 适配现代工具的模块解析策略
  • noEmit: true — 我们只用 tsx 直接运行,不需要 tsc 输出编译产物

1.4 环境变量

创建 .env 文件(敏感信息不要提交到 Git):

ANTHROPIC_API_KEY=sk-your-key-hereANTHROPIC_BASE_URL=https://your-proxy-server.com/anthropicMODEL_NAME=qwen3.7-plus

这里用了一个小技巧:通过 ANTHROPIC_BASE_URL 指向一个袋里服务器,底层实际跑的是 qwen3.7-plus 模型,但因为接口兼容 Anthropic 协议,所以可以直接用 ChatAnthropic 来调用。

1.5 项目结构

最终的文件结构很简洁:

lingshi/├── src/│ ├── tools.ts# 工具定义(计算器、获取时间)│ ├── agents.ts # Agent 创建与配置│ └── index.ts# 入口文件,运行测试├── .env# 环境变量(API Key 等)├── package.json└── tsconfig.json

三个文件各司其职,下面逐一展开。

2. 理解 Agent:它和普通 LLM 调用有什么不同?

在写代码之前,先搞清楚一个核心问题:Agent 到底是什么?

普通 LLM 调用

用户输入 → LLM → 一次性返回结果,结束

LLM 就是一个"文本接龙机器"——你给它一段 prompt,它吐出一段回复,完事。它不能帮你查天气,不能帮你算数学,不能访问任何外部系统。

Agent 调用

用户输入 LLM 需要工具吗?├─ Yes 执行工具 结果喂回 LLM 继续判断...└─ No 返回最终回复

Agent 在 LLM 的基础上加了一个循环:

while (LLM 觉得还需要工具) {执行工具 → 结果喂回 LLM}return LLM 的最终回答

举个例子:用户问"帮我算 128 × 47"

  1. LLM 看到有计算器工具,决定调用 → calculator({ a:128, b:47, operation:'multiply' })
  2. 工具返回 "128 multiply 47 = 6016"
  3. LLM 拿到结果,生成自然语言回复:"128 × 47 = 6016"

Agent = LLM + 工具 + 循环,这就是全部。

3. 定义工具:让 AI 拥有"手"

工具的完整代码在 src/tools.ts 中。

3.1 Tool Calling 原理

Tool Calling 是 Agent 的核心机制,它的工作流程分 5 步:

  1. 用户发送消息 → LLM 分析是否需要调用工具
  2. LLM 返回 tool_call → 包含工具名 + 参数 JSON
  3. Agent 框架执行工具函数 → 拿到结果
  4. 工具结果喂回 LLM → 作为新消息
  5. LLM 综合结果 → 生成最终回复

第 2 步和第 3 步之间可能存在多次循环,这就是所谓的 Agent Loop。

3.2 计算器工具

import { tool } from '@langchain/core/tools';import { z } from 'zod';export const calculatorTool = tool(// 第一个参数:工具的执行函数async ({ a, b, operation }) => {let result: number;switch (operation) {case 'add':result = a + b; break;case 'subtract': result = a - b; break;case 'multiply': result = a * b; break;case 'divide':if (b === 0) return '错误:除数不能为零';result = a / b;break;default:return `错误:不支持的操作 "${operation}"`;}return `${a} ${operation} ${b} = ${result}`;},// 第二个参数:工具的元信息{name: 'calculator',description: '对两个数字执行四则运算(加、减、乘、除)',schema: z.object({a: z.number().describe('第一个数字'),b: z.number().describe('第二个数字'),operation: z.enum(['add', 'subtract', 'multiply', 'divide']).describe('要执行的操作:add(加)、subtract(减)、multiply(乘)、divide(除)'),}),});

每个工具有三要素:

要素说明
name工具的唯一标识,LLM 通过这个名字来调用
description告诉 LLM 这个工具能干什么,LLM 据此决定是否使用
schemaZod 定义的参数类型,框架会转成 JSON Schema 发给 LLM

3.3 为什么用 Zod?

Zod 是一个 TypeScript-first 的运行时类型校验库。在 Deep Agents 中,Zod schema 承担了一个关键职责:告诉 LLM 该怎么传参数。

z.object({a: z.number().describe('第一个数字'),b: z.number().describe('第二个数字'),operation: z.enum(['add', 'subtract', 'multiply', 'divide']).describe('要执行的操作'),})

框架内部会把这段 Zod schema 转换成 JSON Schema,大致长这样:

{"type": "object","properties": {"a": { "type": "number", "description": "第一个数字" },"b": { "type": "number", "description": "第二个数字" },"operation": {"type": "string","enum": ["add", "subtract", "multiply", "divide"],"description": "要执行的操作"}},"required": ["a", "b", "operation"]}

LLM 看到这份 JSON Schema,就知道调用 calculator 时需要传 ab(数字)和 operation(枚举字符串)。.describe() 里的描述是 LLM 理解参数含义的关键——不写描述,LLM 就只能猜。

3.4 无参数工具:获取当前时间

不是所有工具都需要参数。获取当前时间就是一个典型:

export const getCurrentTimeTool = tool(async () => {const now = new Date();return `当前时间不好说:${now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`;},{name: 'get_current_time',description: '获取当前的系统时间(北京时间)',schema: z.object({}),// 空 schema → LLM 知道不用传参});

z.object({}) 即空对象 schema,LLM 看到它就知道调用时不用传任何参数。

4. 创建 Agent:把大脑和手连起来

代码在 src/agents.ts 中。

4.1 配置 ChatModel

import { ChatAnthropic } from '@langchain/anthropic';const model = new ChatAnthropic({model: process.env.MODEL_NAME || 'qwen3.7-plus',anthropicApiKey: process.env.ANTHROPIC_API_KEY,anthropicApiUrl: process.env.ANTHROPIC_BASE_URL,streaming: true,maxTokens: 10000,thinking: {type: 'enabled',budget_tokens: 5000,},});

ChatAnthropic 是 LangChain 的 ChatModel 实现之一。ChatModel 是 LangChain 对"对话模型"的统一抽象,提供两个核心方法:

  • .invoke(messages) — 同步调用,等完整回复
  • .stream(messages) — 流式调用,逐 token 返回

这里有两个值得注意的配置:

  • streaming: true — 开启模型层的流式输出,后面会详细讲
  • thinking — 开启 Extended Thinking,让模型在回复前先进行内部推理,最后一节会展开

4.2 创建 Agent 实例

import { createDeepAgent } from 'deepagents';import { MemorySaver } from '@langchain/langgraph';export const agent = createDeepAgent({model,tools: [calculatorTool, getCurrentTimeTool],systemPrompt: '你是一个乐于助人的 AI 助手。当用户需要进行数学计算或查询时间时,请使用相应的工具来完成。',checkpointer: new MemorySaver(),});

createDeepAgent 创建了一个 LangGraph 图结构的 Agent,内部流程:

[用户消息] → [LLM] → 要调工具?├─ Yes → [执行工具] → 回到 LLM└─ No→ [返回最终回复]

四个参数的含义:

参数说明
modelChatModel 实例,Agent 的"大脑"
tools工具数组,Agent 自主选择调用哪个
systemPrompt系统提示词,定义 Agent 的角色
checkpointer记忆存储器,MemorySaver 是内存版(重启丢失),生产环境可换 PostgresSaver

5. 流式输出:别让用户干等着

代码在 src/index.ts 中。

5.1 invoke vs stream

LangChain Agent 提供两种调用方式:

  • agent.invoke() — 等 Agent 完成所有工具调用后才返回完整结果(阻塞式)
  • agent.stream() — 每产生一条消息就立刻 yield 出来(流式)

对于一个需要调用工具的 Agent,invoke 可能要等好几秒才有输出。而 stream 能让用户实时看到 AI 在"打字",体验完全不同。

5.2 流式消息的类型

使用 stream() 配合 streamMode: 'messages',每次 yield 的是 [message, metadata] 元组:

const stream = await agent.stream({ messages: [{ role: 'user', content: '帮我计算 128 乘以 47 等于多少' }] },{ ...config, streamMode: 'messages' },);for await (const [message] of stream) {console.log(message);// 会看到各种类型的消息}

message 是 LangChain 的消息对象,通过 message._getType() 判断类型:

类型含义
'ai'LLM 的输出(可能包含文本 + tool_call)
'tool'工具执行后的返回结果
'human'用户消息(stream 中一般不出现)

5.3 content 的两种形态

AI 消息的 content 字段有两种形态,这是个容易踩坑的地方:

形态一:字符串(没有调用工具时的纯文本回复)

message.content === "128 × 47 = 6016"

形态二:数组(调用了工具时,包含多种 block)

message.content === [{ type: 'text', text: '计算结果是...' },{ type: 'tool_use', id: '...', name: 'calculator', input: {...} },]

所以处理流式消息时,两种情况都要兼顾:

async function printStream(stream: AsyncIterable<[any, any]>) {for await (const [message] of stream) {// 只处理 AI 消息,跳过 tool / humanif (message?._getType?.() === 'ai') {// 情况 1:content 是字符串if (typeof message.content === 'string' && message.content) {process.stdout.write(message.content);}// 情况 2:content 是数组,遍历找 text blockelse if (Array.isArray(message.content)) {for (const block of message.content) {if (block.type === 'text' && block.text) {process.stdout.write(block.text);}}}}}}

6. 多轮对话:让 Agent 记住上下文

6.1 thread_id 与记忆

普通 LLM 每次调用都是独立的,它不记得你上一句说了什么。Agent 通过 checkpointer(记忆存储器)解决这个问题。

const config = { configurable: { thread_id: 'session-1' } };

MemorySaverthread_id 存储对话历史。同一个 thread_id 的所有消息会被累积存储,Agent 每次调用时都能看到之前的完整对话。

6.2 实际效果

// 第一轮:计算器await agent.stream({ messages: [{ role: 'user', content: '帮我计算 128 乘以 47 等于多少' }] },{ ...config, streamMode: 'messages' },);// Agent 回复:"128 × 47 = 6016"// 第二轮:故意质疑await agent.stream({ messages: [{ role: 'user', content: '算的不对吧' }] },{ ...config, streamMode: 'messages' },);// Agent 会回顾之前的计算,重新审视结果// 因为它"记得"上一轮自己算了什么

第二轮的"算的不对吧"没有提供任何数字信息,但 Agent 能理解这是在质疑上一轮的计算结果,这就是 thread_id + MemorySaver 的效果。

7. Extended Thinking:看见模型的思考过程

这是最后加上的一个进阶功能——让模型在给出最终答案之前,先展示它的内部推理过程。

7.1 什么是 Extended Thinking?

Extended Thinking 是 Anthropic 提供的一个能力:模型在生成最终回复之前,会先进行一段"内心独白"(thinking),展示它是怎么一步步推理的。

对于用户来说,这就像一个"透明窗口"——你能看到 AI 在"想什么",而不只是看到最终答案。

7.2 开启 Thinking

ChatAnthropic 配置中加入 thinking 参数:

const model = new ChatAnthropic({model: 'qwen3.7-plus',// ...maxTokens: 10000, // thinking 模式下必须显式设置thinking: {type: 'enabled',budget_tokens: 5000,// thinking 阶段最多消耗 5000 token},});

注意:开启 thinking 后,maxTokens 必须显式设置,这是 Anthropic API 的硬性要求。

7.3 处理 thinking block

开启 thinking 后,AI 消息的 content 数组中会多出一种 block 类型:

message.content === [{ type: 'thinking', thinking: '用户让我算 128 × 47,我需要用计算器工具...' },{ type: 'tool_use', ... },{ type: 'text', text: '128 × 47 = 6016' },]

printStream 中新增对 thinking block 的处理:

for (const block of message.content) {// thinking block:模型的内部推理(用灰色显示)if (block.type === 'thinking' && block.thinking) {process.stdout.write(`x1b[90m[思考] ${block.thinking}x1b[0m`);}// text block:模型的最终文本回复if (block.type === 'text' && block.text) {process.stdout.write(block.text);}}

x1b[90m 是 ANSI 转义码,让 thinking 内容以灰色显示,和最终的文本回复在视觉上区分开。

7.4 运行效果

--- 测试 1:计算器工具 ---用户:帮我计算 128 乘以 47 等于多少助手:[思考] 用户想要计算 128 乘以 47,这是一个乘法运算,我应该使用 calculator 工具...128 × 47 = 6016

灰色部分是模型的推理过程,正常颜色是最终答案。

8. 回顾与展望

我们做了什么

从一个空文件夹开始,一步步搭建了:

  1. TypeScript 项目骨架 — pnpm init + tsconfig.json + tsx 开发环境
  2. 两个工具 — 计算器(有参数)和获取时间(无参数),理解了 Zod schema 的作用
  3. 一个 Agent — 用 createDeepAgent 把 LLM + 工具 + 循环串起来
  4. 流式输出 — agent.stream() + streamMode: 'messages',踩过了 content 类型的坑
  5. 多轮对话 — thread_id + MemorySaver 实现上下文记忆
  6. Extended Thinking — 让模型的推理过程可视化

完整运行

pnpm dev

输出三个测试场景:计算器工具调用、多轮对话上下文记忆、时间工具调用。

后续可以做什么

主线
  • 让 agent 能读写文件、执行代码、操作文件系统。
支线
  • 加入更多工具(搜索、数据库查询、API 调用)
  • MemorySaver 换成持久化存储
  • 接入真实的 Anthropic Claude 模型体验原生 thinking
  • 给 Agent 加错误处理和重试机制
  • 构建一个 Web 界面,把流式输出通过 SSE 推到前端

Agent 的世界刚刚打开,这个项目只是一个起点。

点击查看更多
推荐专题
热门阅读