普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月17日首页

使用 LangChain.js 在node端 连接glm大模型示例

作者 kevinIcoding
2026年1月17日 12:47

使用 LangChain 在后端连接大模型:实践指南 🚀

本文以实战项目代码为例,包含完整后端接入、流式(SSE)实现、前端接收示例与调试方法,读者可直接复制运行。


介绍 ✨

随着大模型在各类应用中的普及,后端如何稳健地接入并把模型能力以 API/流式方式对外提供,成为常见需求。本文基于 LangChain(JS)演示如何在 Node.js 后端(Koa)中:

  • 初始化并调用大模型(示例使用智谱 GLM 的接入方式)
  • 支持普通请求与流式(Server-Sent Events,SSE)响应
  • 在前端用 fetch 读取流并实现打字机效果

适用人群:熟悉 JS/TS、Node.js、前端基本知识,想把模型能力放到后端并对外提供 API 的工程师。


一、准备与依赖 🧩

环境:Node.js 16+。

安装依赖(Koa 示例):

npm install koa koa-router koa-bodyparser @koa/cors dotenv
npm install @langchain/openai @langchain/core

在项目根创建 .env

ZHIPU_API_KEY=你的_api_key_here
PORT=3001

提示:不同模型提供方的 baseURL 与认证字段会不同,请根据提供方文档调整。


二、后端:服务封装(chatService)🔧

把模型调用封装到服务层,提供普通调用与流式调用接口:

// backend/src/services/chatService.js
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";

const chatService = {
  llm: null,

  init() {
    if (!this.llm) {
      this.llm = new ChatOpenAI({
        openAIApiKey: process.env.ZHIPU_API_KEY,
        modelName: "glm-4.5-flash",
        temperature: 0.7,
        configuration: { baseURL: "https://open.bigmodel.cn/api/paas/v4/" },
      });
    }
  },

  async sendMessage(message, conversationHistory = []) {
    this.init();
    const messages = [
      new SystemMessage("你是一个有帮助的 AI 助手,使用中文回答问题。"),
      ...conversationHistory.map((msg) =>
        msg.role === "user"
          ? new HumanMessage(msg.content)
          : new SystemMessage(msg.content),
      ),
      new HumanMessage(message),
    ];

    try {
      const response = await this.llm.invoke(messages);
      return {
        success: true,
        content: response.content,
        usage: response.usage_metadata,
      };
    } catch (error) {
      console.error("聊天服务错误:", error);
      return { success: false, error: error.message };
    }
  },

  async sendMessageStream(message, conversationHistory = []) {
    this.init();
    const messages = [
      new SystemMessage("你是一个有帮助的 AI 助手,使用中文回答问题。"),
      ...conversationHistory.map((msg) =>
        msg.role === "user"
          ? new HumanMessage(msg.content)
          : new SystemMessage(msg.content),
      ),
      new HumanMessage(message),
    ];

    try {
      // 假设 llm.stream 返回异步迭代器,逐 chunk 返回 { content }
      const stream = await this.llm.stream(messages);
      return { success: true, stream };
    } catch (error) {
      console.error("流式聊天服务错误:", error);
      return { success: false, error: error.message };
    }
  },
};

export default chatService;

说明:实际 SDK 接口名(如 invokestream)请依据你所用的 LangChain / provider 版本调整。


三、控制器:普通与 SSE 流式(Koa)🌊

SSE 要点:需要直接写原生 res,并设置 ctx.respond = false,防止 Koa 在中间件链结束时覆盖响应或返回 404。

// backend/src/controllers/chatController.js
import chatService from "../services/chatService.js";

const chatController = {
  async sendMessage(ctx) {
    try {
      const { message, conversationHistory = [] } = ctx.request.body;
      if (!message) {
        ctx.status = 400;
        ctx.body = { success: false, error: "消息内容不能为空" };
        return;
      }
      const result = await chatService.sendMessage(
        message,
        conversationHistory,
      );
      if (result.success) ctx.body = result;
      else {
        ctx.status = 500;
        ctx.body = result;
      }
    } catch (error) {
      ctx.status = 500;
      ctx.body = { success: false, error: "服务器内部错误" };
    }
  },

  async sendMessageStream(ctx) {
    try {
      const { message, conversationHistory = [] } = ctx.request.body;
      if (!message) {
        ctx.status = 400;
        ctx.body = { success: false, error: "消息内容不能为空" };
        return;
      }

      const result = await chatService.sendMessageStream(
        message,
        conversationHistory,
      );
      if (!result.success) {
        ctx.status = 500;
        ctx.body = result;
        return;
      }

      ctx.set("Content-Type", "text/event-stream");
      ctx.set("Cache-Control", "no-cache");
      ctx.set("Connection", "keep-alive");
      ctx.status = 200;
      // 关键:让我们直接操作 Node 原生 res
      ctx.respond = false;

      for await (const chunk of result.stream) {
        const content = chunk.content;
        if (content) ctx.res.write(`data: ${JSON.stringify({ content })}\n\n`);
      }

      ctx.res.write("data: [DONE]\n\n");
      ctx.res.end();
    } catch (error) {
      console.error("流式控制器错误:", error);
      ctx.status = 500;
      ctx.body = { success: false, error: "服务器内部错误" };
    }
  },
};

export default chatController;

四、路由与启动 🌐

// backend/src/routes/index.js
import Router from "koa-router";
import chatController from "../controllers/chatController.js";
const router = new Router({ prefix: "/api" });
router.post("/chat", chatController.sendMessage);
router.post("/chat/stream", chatController.sendMessageStream);
export default router;

// backend/src/app.js
import dotenv from "dotenv";
import Koa from "koa";
import bodyParser from "koa-bodyparser";
import cors from "@koa/cors";
import router from "./routes/index.js";

dotenv.config();
const app = new Koa();
app.use(cors({ origin: "*", credentials: true }));
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(process.env.PORT || 3001, () => console.log("Server running"));

五、前端:接收 SSE 流并实现“打字机”效果 ⌨️

前端用 fetch + ReadableStream 读取 SSE 后端发送的 chunk(格式为 data: {...}\n\n)。下面给出简洁示例:

// frontend/src/services/chatService.ts (核心片段)
export const sendMessageStream = async (
  message,
  conversationHistory,
  onChunk,
  onComplete,
  onError,
) => {
  try {
    const response = await fetch("/api/chat/stream", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message, conversationHistory }),
    });

    if (!response.ok) throw new Error("网络请求失败");
    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    let buffer = "";
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value);
      // 简单按行解析 SSE data: 行
      const lines = chunk.split("\n");
      for (const line of lines) {
        if (line.startsWith("data: ")) {
          const data = line.slice(6);
          if (data === "[DONE]") {
            onComplete();
            return;
          }
          try {
            const parsed = JSON.parse(data);
            if (parsed.content) onChunk(parsed.content);
          } catch (e) {}
        }
      }
    }
  } catch (err) {
    onError(err.message || "流式请求失败");
  }
};

打字机效果思路(前端)📌:

  • 后端 chunk 通常是按小段返回,前端把每个 chunk 追加到 buffer
  • 用一个定时器以固定速度(如 20–40ms/字符)把 buffer 的字符逐个移动到展示内容,使文本逐字出现。
  • onComplete 时快速显示剩余字符并停止定时器。

你可以参考项目中 App.tsx 的实现(已实现逐 chunk 追加与打字机渲染逻辑)。


App.tsx

import React, { useState } from "react";
import MessageList from "./components/MessageList";
import ChatInput from "./components/ChatInput";
import { chatService, Message } from "./services/chatService";
import "./App.css";

const App: React.FC = () => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);

  const handleSendMessage = async (message: string) => {
    const userMessage: Message = { role: "user", content: message };
    setMessages((prev) => [...prev, userMessage]);
    setIsStreaming(true);

    let assistantContent = "";
    const conversationHistory = messages;

    await chatService.sendMessageStream(
      message,
      conversationHistory,
      (chunk: string) => {
        assistantContent += chunk;
        setMessages((prev) => {
          const newMessages = [...prev];
          const lastMessage = newMessages[newMessages.length - 1];
          if (lastMessage && lastMessage.role === "assistant") {
            lastMessage.content = assistantContent;
          } else {
            newMessages.push({ role: "assistant", content: assistantContent });
          }
          return newMessages;
        });
      },
      () => {
        setIsStreaming(false);
      },
      (error: string) => {
        console.error("流式响应错误:", error);
        setMessages((prev) => [
          ...prev,
          { role: "assistant", content: "抱歉,发生了错误,请稍后重试。" },
        ]);
        setIsStreaming(false);
      },
    );
  };

  return (
    <div className="app">
      <header className="app-header">
        <h1>🤖 LangChain + 智谱 GLM</h1>
        <p>AI 聊天助手</p>
      </header>
      <main className="app-main">
        <MessageList messages={messages} isStreaming={isStreaming} />
        <ChatInput onSendMessage={handleSendMessage} disabled={isStreaming} />
      </main>
    </div>
  );
};

export default App;

MessageList

import React, { useRef, useEffect } from "react";
import { Message } from "../services/chatService";

interface MessageListProps {
  messages: Message[];
  isStreaming: boolean;
}

const MessageList: React.FC<MessageListProps> = ({ messages, isStreaming }) => {
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages, isStreaming]);

  return (
    <div className="message-list">
      {messages.length === 0 ? (
        <div className="empty-state">
          <p>👋 欢迎使用 LangChain + 智谱 GLM 聊天助手!</p>
          <p>开始提问吧,我会尽力帮助你 💬</p>
        </div>
      ) : (
        messages.map((msg, index) => (
          <div key={index} className={`message ${msg.role}`}>
            <div className="message-avatar">
              {msg.role === "user" ? "👤" : "🤖"}
            </div>
            <div className="message-content">
              <div className="message-role">
                {msg.role === "user" ? "用户" : "AI 助手"}
              </div>
              <div className="message-text">{msg.content}</div>
            </div>
          </div>
        ))
      )}
      {isStreaming && (
        <div className="message assistant">
          <div className="message-avatar">🤖</div>
          <div className="message-content">
            <div className="message-role">AI 助手</div>
            <div className="message-text streaming">
              <span className="typing-indicator">...</span>
            </div>
          </div>
        </div>
      )}
      <div ref={messagesEndRef} />
    </div>
  );
};

export default MessageList;

六、调试建议与常见坑 ⚠️

  • 404:确认前端请求路径 /api/chat/stream 与路由前缀一致;开发时若使用 Vite,请在 vite.config.ts 配置 proxy 到后端端口。
  • SSE 返回 404/空响应:确认控制器里 ctx.respond = false 已设置,并且在设置 header 后立即开始 ctx.res.write
  • headers already sent:不要在写入原生 res 后再次设置 ctx.bodyctx.status
  • CORS:若跨域,确保后端 CORS 配置允许 Content-TypeAuthorization 等必要 header。

七、快速上手测试命令 🧪

启动后端:

# 在 backend 目录下
node src/app.js
# 或使用 nodemon
npx nodemon src/app.js

用 curl 测试流式(查看快速返回流):

curl -N -H "Content-Type: application/json" -X POST http://localhost:3001/api/chat/stream -d '{"message":"你好","conversationHistory":[]} '

你应能看到类似:

data: {"content":"你"}
data: {"content":"好"}
data: [DONE]

前端AI应用开发深入理解 FunctionCall:让 LLM 拥有"超能力"的完整指南

作者 kevinIcoding
2026年1月17日 10:05

想象一下,如果你的 AI 助手不仅能聊天,还能帮你查询实时天气、调用 API、执行数据库查询,那会是什么体验?FunctionCall 技术正是实现这一目标的关键!

🌟 什么是 FunctionCall?

FunctionCall(函数调用)是一种让大语言模型(LLM)能够调用外部工具函数的技术。它打破了传统 LLM 只能基于训练数据回答问题的限制,让 AI 能够:

  • 🔍 查询实时数据(如天气、股票价格)
  • 📊 访问数据库获取最新信息
  • 🔗 调用第三方 API 服务
  • ⚙️ 执行特定的业务逻辑

简单来说,FunctionCall 就像是给 LLM 装上了"手脚",让它不仅能"思考",还能"行动"!

🚀 为什么需要 FunctionCall?

传统的 LLM 有一个明显的局限:它只能基于训练时的数据回答问题。这意味着:

❌ 无法获取实时信息(比如今天的天气) ❌ 无法访问你的私有数据 ❌ 无法执行具体的操作(如发送邮件、创建订单)

而 FunctionCall 完美解决了这些问题!它让 LLM 成为了一个智能的"调度器",能够:

  • 理解用户的自然语言需求
  • 判断需要调用哪些工具
  • 自动提取函数参数
  • 将执行结果转化为自然语言回答

FunctionCall 完整工作流程

让我们通过一个实际例子来理解整个流程。假设用户问:"北京今天天气怎么样?"

🔧 步骤 1:定义工具描述

首先,我们需要告诉 LLM 有哪些工具可以使用。这就像给 LLM 提供一份"工具说明书":

const tools = [
  {
    type: "function",
    function: {
      name: "get_current_weather",
      description: "获取指定城市当前的实时天气",
      parameters: {
        type: "object",
        properties: {
          city: {
            type: "string",
            description: "城市名称",
          },
          unit: {
            type: "string",
            enum: ["摄氏度", "华氏度"],
            description: "温度单位",
          },
        },
        required: ["city"],
      },
    },
  },
];

这里的描述非常重要!LLM 会根据 description 来判断何时需要调用这个函数,根据 parameters 来提取用户输入中的参数值。

⚙️ 步骤 2:实现实际函数

定义好工具描述后,我们需要实现真正的函数逻辑:

function get_current_weather(city, unit = "摄氏度") {
  // 这里可以调用外部 API 来获取真实天气数据
  console.log(`正在获取 ${city} 的天气...`);
  const weather = {
    city: city,
    temperature: "25",
    unit: unit,
    forecast: "晴朗",
  };
  return JSON.stringify(weather);
}

这个函数可以调用任何你需要的 API 或执行任何业务逻辑。

🔗 步骤 3:建立函数映射

为了方便调用,我们创建一个映射对象:

const availableFunctions = {
  get_current_weather: get_current_weather,
};

这样 LLM 返回函数名后,我们就能快速找到对应的函数并执行。

🚀 步骤 4:发起对话

现在,我们将用户的问题和可用工具一起发送给 LLM:

const response = await zhipuClient.createCompletions({
  model: llmModel,
  messages: [
    {
      role: "user",
      content: "北京今天天气怎么样?",
    },
  ],
  tools: tools, // 告诉 LLM 有哪些工具可用
});

🤔 步骤 5:LLM 智能决策

LLM 会分析用户的问题,并做出智能判断:

情况 A:需要调用工具

{
  "tool_calls": [
    {
      "id": "call_123",
      "function": {
        "name": "get_current_weather",
        "arguments": "{\"city\": \"北京\", \"unit\": \"摄氏度\"}"
      }
    }
  ]
}

情况 B:不需要调用工具

{
  "content": "我是 AI 助手,无法直接回答天气问题。"
}

这就是 FunctionCall 的神奇之处:LLM 自动判断是否需要调用工具,并从自然语言中提取参数!

💻 步骤 6:执行本地函数

当 LLM 决定调用工具时,我们解析参数并执行函数:

const toolCall = message.tool_calls[0];
const functionName = toolCall.function.name;
const functionArgs = JSON.parse(toolCall.function.arguments);

const functionResponse = availableFunctions[functionName](
  functionArgs.city,
  functionArgs.unit
);

执行结果可能是:

{
  "city": "北京",
  "temperature": "25",
  "unit": "摄氏度",
  "forecast": "晴朗"
}

🔄 步骤 7:回传结果给 LLM

将函数执行结果作为上下文再次发送给 LLM:

const secondResponse = await zhipuClient.createCompletions({
  model: llmModel,
  messages: [
    { role: "user", content: "北京今天天气怎么样?" }, // 原始问题
    message, // LLM 的工具调用请求
    {
      role: "tool", // 工具执行结果
      tool_call_id: toolCall.id,
      content: functionResponse,
    },
  ],
});

这一步非常关键!LLM 需要看到完整的对话历史,包括:

  • 用户的问题
  • 自己的工具调用决策
  • 工具的执行结果

✅ 步骤 8:生成最终回答

最后,LLM 基于函数执行结果,生成自然语言的回答:

根据查询结果,北京今天的天气是晴朗,温度为 25 摄氏度。今天是个好天气,适合外出活动!

🎯 FunctionCall 的核心优势

1. 智能决策 🧠

LLM 自动判断是否需要调用工具,无需复杂的规则判断。比如:

  • 用户问"北京天气怎么样?" → 调用天气查询函数
  • 用户问"什么是人工智能?" → 直接回答,无需调用工具

2. 参数解析 📝

LLM 能够从自然语言中精准提取参数:

  • "北京今天天气怎么样?" → city="北京"
  • "查一下上海的天气,用华氏度" → city="上海", unit="华氏度"
  • "纽约的天气如何" → city="纽约"

3. 结果整合 💬

LLM 将结构化的函数结果转化为自然的语言回答,让用户体验更加流畅。

4. 多轮对话 🔄

支持连续的工具调用和上下文延续,可以进行复杂的任务链。

� 实际应用场景

场景 1:智能客服助手

// 工具:查询订单状态
function get_order_status(orderId) { ... }

// 工具:处理退款
function process_refund(orderId, reason) { ... }

// 用户:"我的订单 #12345 怎么还没到?"
// LLM 自动调用 get_order_status("12345")
// 回答:"您的订单 #12345 已发货,预计明天送达"

场景 2:数据分析助手

// 工具:查询数据库
function query_database(sql) { ... }

// 工具:生成图表
function generate_chart(data, type) { ... }

// 用户:"帮我看看上个月的销售数据"
// LLM 自动调用 query_database 和 generate_chart

场景 3:办公自动化

// 工具:发送邮件
function send_email(to, subject, body) { ... }

// 工具:创建日程
function create_calendar_event(title, time) { ... }

// 用户:"帮我给张三发封邮件约明天开会"
// LLM 自动调用 send_email 和 create_calendar_event

🛠️ 快速开始

1. 安装依赖

npm install zhipu-sdk-js dotenv

2. 配置环境变量

创建 .env 文件:

ZHIPUAI_API_KEY=your_api_key_here

3. 编写代码

import ZhipuAI from "zhipu-sdk-js";
import "dotenv/config";

const zhipuClient = new ZhipuAI({
  apiKey: process.env.ZHIPUAI_API_KEY,
});

// 定义工具、实现函数、运行对话...

4. 运行示例

node index.js

📚 技术栈

  • Node.js: 运行环境
  • zhipu-sdk-js: 智谱 AI 官方 SDK
  • GLM-4.5-Flash: 高性能大语言模型
  • dotenv: 环境变量管理

🔮 未来展望

FunctionCall 技术正在快速发展,未来可能支持:

  • 🤖 多工具并行调用
  • 📊 复杂的工具调用链
  • 🎯 自适应工具选择
  • 🔒 更安全的权限控制

代码示例

// index.js
import ZhipuAI from "zhipu-sdk-js";

import "dotenv/config";

// 步骤 1: 定义你的工具函数
const tools = [
  {
    type: "function",
    function: {
      name: "get_current_weather",
      description: "获取指定城市当前的实时天气",
      parameters: {
        type: "object",
        properties: {
          city: {
            type: "string",
            description: "城市名称",
          },
          unit: {
            type: "string",
            enum: ["摄氏度", "华氏度"],
            description: "温度单位",
          },
        },
        required: ["city"],
      },
    },
  },
];

// 步骤 2: 创建一个假想的函数来执行工具
function get_current_weather(city, unit = "摄氏度") {
  // 这里可以调用外部 API 来获取真实天气数据
  // 为了简化,我们直接返回一个模拟值
  console.log(`正在获取 ${city} 的天气...`);
  const weather = {
    city: city,
    temperature: "25",
    unit: unit,
    forecast: "晴朗",
  };
  return JSON.stringify(weather);
}

// 定义一个映射,将函数名和实际函数绑定
const availableFunctions = {
  get_current_weather: get_current_weather,
};

const zhipuClient = new ZhipuAI({
  apiKey: process.env.ZHIPUAI_API_KEY,
});

const llmModel = "glm-4.5-flash";
async function runConversation() {
  const userMessage = "北京今天天气怎么样?";

  // 向 LLM 发送用户问题,并提供可用的工具
  const response = await zhipuClient.createCompletions({
    model: llmModel,
    messages: [
      {
        role: "user",
        content: userMessage,
      },
    ],
    tools: tools,
  });
  debugger;
  const message = response.choices[0].message;

  // 步骤 3: 检查 LLM 是否决定调用函数
  if (message.tool_calls && message.tool_calls.length > 0) {
    const toolCall = message.tool_calls[0];
    const functionName = toolCall.function.name;
    const functionArgs = JSON.parse(toolCall.function.arguments);

    // 步骤 4: 在本地执行函数
    const functionResponse = availableFunctions[functionName](
      functionArgs.city,
      functionArgs.unit,
    );

    // 再次调用 LLM,将函数执行结果作为上下文
    const secondResponse = await zhipuClient.createCompletions({
      model: llmModel,
      messages: [
        { role: "user", content: userMessage },
        message, // 将 LLM 第一次的响应也作为上下文
        {
          role: "tool",
          tool_call_id: toolCall.id,
          content: functionResponse, // 将函数执行结果作为上下文
        },
      ],
    });

    // 最终返回 LLM 基于函数结果生成的回答
    console.log(secondResponse.choices[0].message.content);
  } else {
    // 如果 LLM 没决定调用函数,直接返回 LLM 的回答
    console.log(message.content);
  }
}

runConversation();
{
  "name": "function_call_llm",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^17.2.3",
    "zhipu-sdk-js": "^1.0.0"
  }
}

📝 总结

FunctionCall 是连接 LLM 与现实世界的桥梁,它让 AI 从"聊天机器人"进化为"智能助手"。通过掌握这项技术,你可以:

  1. 构建更强大的 AI 应用
  2. 提供更智能的用户体验
  3. 实现更复杂的业务场景

现在就开始尝试吧! 让你的 AI 助手拥有真正的"超能力"!🚀


💡 小贴士:本文档配套了完整的示例代码,你可以直接运行体验 FunctionCall 的强大功能。如有问题,欢迎交流讨论!

❌
❌