1. 模型调用

LangChain.js 里,所有能"对话"的模型都叫 ChatModel(聊天模型)。DeepSeek 的接入类是 ChatDeepSeek

import { ChatDeepSeek } from '@langchain/deepseek'

const model = new ChatDeepSeek({
    model: 'deepseek-v4-flash',  // 模型名
    apiKey: process.env.DEEPSEEK_API_KEY, // 模型中的key
})

调用方式

invoke(同步等结果):

const response = await model.invoke('用一句话解释什么是 Node.js?')
console.log(response.content)  // 字符串回答

stream(流式):

const stream = await model.stream('写一首关于秋天的诗')
for await (const chunk of stream) {
    process.stdout.write(chunk.content as string)
}

和 ai SDK 对照

ai SDK LangChain.js
generateText({ model, prompt }) model.invoke(prompt)
streamText({ model, prompt }) model.stream(prompt)

注意一个本质差异:ai SDK 的 generateText 是一个函数,模型只是参数;LangChain.js 的 model 本身就是 Runnable,直接 invoke。这是两种设计哲学——ai SDK 是"函数式",LangChain 是"对象式(OO)"。

完整示例:

import 'dotenv/config'
import { ChatDeepSeek } from '@langchain/deepseek'

// 实例化模型
const model = new ChatDeepSeek({
    model: 'deepseek-v4-flash',
    apiKey: process.env.DEEPSEEK_API_KEY,
})

async function main() {
    console.log('=== 1. invoke:同步等待完整结果 ===\n')
    const response = await model.invoke('用一句话解释什么是 Node.js?')
    console.log('返回类型:', response.constructor.name)   // AIMessage
    console.log('内容:', response.content)                // 字符串回答
    console.log()

    console.log('=== 2. stream:流式逐字输出 ===\n')
    const stream = await model.stream('写一首关于秋天的诗,四行即可')
    for await (const chunk of stream) {
        process.stdout.write(chunk.content as string)
    }
    console.log('\n')
}

main().catch(err => {
    console.error('运行失败:', err)
    process.exit(1)
})

2. 消息与 Prompt

消息类型

LangChain 用不同的消息类表示对话中的不同角色:

import { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages'
const messages = [
    new SystemMessage('你是一个简洁的代码助手'),
    new HumanMessage('什么是 Node.js?'),
    new AIMessage('Node.js 是一个 JS 运行时'),   // 历史对话
    new HumanMessage('它和浏览器 JS 有什么区别?'),
]
const response = await model.invoke(messages)
  • SystemMessage:设定 AI 的角色/规则
  • HumanMessage:用户说的话
  • AIMessage:AI 之前说的话(多轮对话时用)

PromptTemplate

当你要把变量填进 prompt 时,用模板:

import { PromptTemplate } from '@langchain/core/prompts'
const prompt = PromptTemplate.fromTemplate(
    '用一句话解释什么是 {topic}?'
)
// 注意这里是prompt.invoke(),不是模型的.invoke()
const formatted = await prompt.invoke({ topic: 'Docker' })
// → '用一句话解释什么是 Docker?'

ChatPromptTemplate(更常用)

聊天场景用 ChatPromptTemplate,它能一次生成多条消息:

import { ChatPromptTemplate } from '@langchain/core/prompts'
const chatPrompt = ChatPromptTemplate.fromMessages([
    ['system', '你是一个{role}'],
    ['human', '{question}'],
]);
// 注意这里是chatPrompt.invoke(),不是模型的.invoke()
const messages = await chatPrompt.invoke({
    role: '代码助手',
    question: '什么是 TypeScript?',
});
// → [SystemMessage, HumanMessage]

和 ai SDK 对照

ai SDK LangChain.js
messages: [{role, content}] [new HumanMessage(...), ...]
system 顶层参数 new SystemMessage(...) 或 ChatPromptTemplate

ai SDK 用普通对象 {role, content} 表示消息;LangChain 用类实例 HumanMessage。本质一样,形式不同。

重点理解

  • PromptTemplate 的核心价值是"变量填充",让 prompt 可复用。
  • ChatPromptTemplate 是后面 LCEL 链的关键构件——它能和 model 用 pipe 拼起来

完整示例:

import 'dotenv/config'
import { ChatDeepSeek } from '@langchain/deepseek'
import { HumanMessage, AIMessage, SystemMessage } from '@langchain/core/messages'
import { PromptTemplate, ChatPromptTemplate } from '@langchain/core/prompts'

const model = new ChatDeepSeek({
    model: 'deepseek-v4-flash',
    apiKey: process.env.DEEPSEEK_API_KEY,
})

async function main() {
    console.log('=== 1. 消息数组(多轮对话)===\n')
    // 用消息类构造一段对话历史,让模型能"接上"上下文
    const messages = [
        new SystemMessage('你是一个简洁的代码助手,回答不超过两句话'),
        new HumanMessage('什么是 Node.js?'),
        new AIMessage('Node.js 是一个服务端 JavaScript 运行时。'),
        new HumanMessage('它和浏览器里的 JS 有什么区别?'),
    ]
    const response = await model.invoke(messages)
    console.log('回答:', response.content)
    console.log()

    console.log('=== 2. PromptTemplate(单条文本模板)===\n')
    const prompt = PromptTemplate.fromTemplate('用一句话解释什么是 {topic}?')
    const formatted = await prompt.invoke({ topic: 'Docker' })
    console.log('填充后:', formatted.value)   // prompt.invoke 返回 StringPromptValue,用 .value 取字符串
    console.log()

    console.log('=== 3. ChatPromptTemplate(聊天模板,常用)===\n')
    // 一次定义多条消息的模板,变量在运行时填充
    const chatPrompt = ChatPromptTemplate.fromMessages([
        ['system', '你是一个{role},回答要简洁'],
        ['human', '{question}'],
    ])
    const chatMessages = await chatPrompt.invoke({
        role: '代码助手',
        question: '什么是 TypeScript?',
    })
    console.log('生成的消息:')
    console.log(chatMessages.toChatMessages())
    // 直接喂给模型
    const answer = await model.invoke(chatMessages)
    console.log('回答:', answer.content)
}

main().catch(err => {
    console.error('运行失败:', err)
    process.exit(1)
})

3. 输出结构化

让模型输出 JSON 对象而不是自由文本,用 .withStructuredOutput

import { z } from 'zod'
const schema = z.object({
    name: z.string().describe('人名'),
    age: z.number().describe('年龄'),
})
const structuredModel = model.withStructuredOutput(schema)
const result = await structuredModel.invoke('张三今年 25 岁')
// result 是 { name: '张三', age: 25 },直接是对象,不是字符串
console.log(result.name)   // '张三'
console.log(result.age)    // 25
  • .withStructuredOutput(schema) 返回一个新的 model,它的 invoke 输出直接是对象(不是 AIMessage)。
  • schema 用 zod 定义。
  • 字段的 .describe() 很重要——它会被转成 JSON Schema 发给模型,帮助模型理解每个字段该填什么。

完整示例:

import 'dotenv/config'
import { ChatDeepSeek } from '@langchain/deepseek'
import { z } from 'zod'

const model = new ChatDeepSeek({
    model: 'deepseek-chat',
    apiKey: process.env.DEEPSEEK_API_KEY,
})

async function main() {
    console.log('=== 1. 基础结构化输出 ===\n')
    // 定义输出 schema:告诉模型"给我返回一个对象,有 name 和 age 两个字段"
    const personSchema = z.object({
        name: z.string().describe('人名'),
        age: z.number().describe('年龄'),
        occupation: z.string().describe('职业'),
    })

    // withStructuredOutput 返回一个新模型,invoke 直接返回对象(不是 AIMessage)
    const structuredModel = model.withStructuredOutput(personSchema)

    const result = await structuredModel.invoke('张三今年 25 岁,是一名前端工程师')
    console.log('返回类型:', typeof result)         // object
    console.log('完整结果:', result)
    console.log('name:', result.name)                // 直接是字段,不是字符串
    console.log('age:', result.age)
    console.log()

    console.log('=== 2. 更复杂的 schema(数组 + 枚举)===\n')
    const bookListSchema = z.object({
        topic: z.string().describe('主题'),
        books: z.array(z.object({
            title: z.string().describe('书名'),
            reason: z.string().describe('推荐理由'),
        })).describe('推荐的书单'),
        difficulty: z.enum(['入门', '进阶', '专家']).describe('难度级别'),
    })

    const recommender = model.withStructuredOutput(bookListSchema)
    const books = await recommender.invoke('推荐 2 本适合初学者的 TypeScript 书')
    console.log('主题:', books.topic)
    console.log('难度:', books.difficulty)
    console.log('书单:')
    books.books.forEach((b, i) => {
        console.log(`  ${i + 1}. 《${b.title}》- ${b.reason}`)
    })
}

main().catch(err => {
    console.error('运行失败:', err)
    process.exit(1)
})

4. 工具定义

工具是 Agent 的"手脚"。LangChain 用 tool() 函数定义:

import { tool } from '@langchain/core/tools'
import { z } from 'zod'

const getWeather = tool(
    async ({ city }) => {
        return `今天 ${city} 晴,25 度`
    },
    {
        name: 'get_weather',
        description: '查询某个城市的天气',
        schema: z.object({
            city: z.string().describe('城市名'),
        }),
    }
)

注意参数顺序和 ai SDK 不同!

LangChain 的 tool() 第一个参数是 execute 函数,第二个参数是配置对象(含 name/description/schema)。ai SDK 是全部塞一个对象。这是两个库最容易搞混的地方。

// LangChain.js
tool(async (input) => { ... }, { name, description, schema })

// ai SDK(对照)
tool({ description, inputSchema, execute: async (input) => { ... } })

另外注意 schema 字段名:LangChain 叫 schema,ai SDK v6 叫 inputSchema

工具怎么被模型调用?

工具定义好之后,有两种用法:

  1. 手动调用await getWeather.invoke({ city: '北京' }) ——直接执行
  2. 交给 Agent:传给 createReactAgent,让模型决定何时调用(第 8 章讲)

和 ai SDK 对照

维度 ai SDK LangChain.js
参数顺序 全塞一个对象 execute 在前,config 在后
schema 字段名 inputSchema schema
工具本质 普通对象 Runnable(可 invoke)

LangChain 的工具本身是 Runnable,能直接 invoke 执行;ai SDK 的工具只是数据结构,执行靠 SDK 内部。

完整示例:

import 'dotenv/config'
import { ChatDeepSeek } from '@langchain/deepseek'
import { tool } from '@langchain/core/tools'
import { z } from 'zod'

const model = new ChatDeepSeek({
    model: 'deepseek-v4-flash',
    apiKey: process.env.DEEPSEEK_API_KEY,
})

// === 1. 定义工具 ===
// 注意 tool() 的参数顺序:第一个是 execute 函数,第二个是 config 对象
const getWeather = tool(
    async ({ city }) => {
        console.log(`  [工具执行] 查询天气: ${city}`)
        // 模拟数据,实际接天气 API
        const mockData: Record<string, string> = {
            '北京': '晴,25°C',
            '上海': '多云,28°C',
            '深圳': '雷阵雨,30°C',
        }
        return mockData[city] ?? `暂无 ${city} 的天气数据`
    },
    {
        name: 'get_weather',
        description: '查询某个城市的当前天气',
        schema: z.object({
            city: z.string().describe('城市名,如"北京"'),
        }),
    }
)

const getTime = tool(
    async () => {
        console.log(`  [工具执行] 获取当前时间`)
        return new Date().toLocaleString('zh-CN')
    },
    {
        name: 'get_time',
        description: '获取当前时间',
        schema: z.object({}),   // 无参数也要传空对象 schema
    }
)

async function main() {
    console.log('=== 1. 手动执行工具(不走模型)===\n')
    // 工具本身是 Runnable,可以直接 invoke 执行
    const direct = await getWeather.invoke({ city: '北京' })
    console.log('直接执行结果:', direct)
    console.log()

    console.log('=== 2. bindTools 让模型调用工具 ===\n')
    // bindTools 把工具"绑"到模型上,模型看到这些工具后能决定何时调用
    const modelWithTools = model.bindTools([getWeather, getTime])

    const response = await modelWithTools.invoke('北京今天天气怎么样?')
    // 模型不会直接回答天气,而是返回 tool_calls(决定要调哪个工具)
    console.log('模型返回的 tool_calls:')
    console.log(response.tool_calls)
    console.log()

    // === 3. 手动执行模型要求的工具调用 ===
    if (response.tool_calls && response.tool_calls.length > 0) {
        const toolCall = response.tool_calls[0]
        console.log(`模型想调工具: ${toolCall.name},参数: ${JSON.stringify(toolCall.args)}`)
        console.log()

        // 根据 toolCall.name 找到对应工具并执行
        const toolMap: Record<string, typeof getWeather> = {
            'get_weather': getWeather,
            'get_time': getTime, // 这个我们没有去实现它
        }
        const result = await toolMap[toolCall.name].invoke(toolCall.args)
        console.log('工具执行结果:', result)
        console.log()

        // 把工具结果喂回模型,让它基于结果给出最终回答
        // 这一步是 Agent 循环的核心(第 8 章 createReactAgent 会自动做这个循环)
        console.log('把结果喂回模型,让它基于工具结果回答:')
        const { HumanMessage, AIMessage, ToolMessage } = await import('@langchain/core/messages')
        const finalResponse = await model.invoke([
            new HumanMessage('北京今天天气怎么样?'),
            response,   // 模型上一步的输出(含 tool_calls)
            new ToolMessage({
                content: String(result),
                tool_call_id: toolCall.id!,   // 靠这个 ID 把结果和调用配对
            }),
        ])
        console.log(finalResponse.content)
    }
}

main().catch(err => {
    console.error('运行失败:', err)
    process.exit(1)
})