3.前端使用Node + Express + Langchain + LangSmith实现服务启动+全链路监控

145 阅读3分钟

前言:本地大模型 + 全链路监控,才是生产级 AI 服务的标配

随着 Ollama 让本地大模型(如 Llama3、Qwen、Gemma)的部署成本大幅降低,越来越多开发者基于 Node+Express+LangChain 搭建私有化 AI 服务。但本地模型落地时容易遇到核心痛点:

  • LangChain 1.0 版本 API 重构后,如何适配 Ollama 做标准化集成?
  • 本地模型调用耗时、提示词输出 / 返回结果无法追踪,出问题只能盲调?
  • 不知道本地模型占用多少 CPU / 内存,服务宕机无预警?
  • LangSmith 如何监控本地 Ollama 模型的链执行全流程?

本文基于 LangChain 1.0 核心 API,结合 Ollama 本地大模型,从「标准化服务启动」和「全链路监控」两大维度,搭建可落地的私有化 AI 服务:既解决本地模型的集成适配问题,又通过 LangSmith 实现链执行追踪,配套日志、指标、健康检查体系,让本地大模型服务也能做到 “可观测、可追溯、可预警”。

一、前置准备:Ollama 环境与依赖安装

1.1 安装 Ollama 并拉取本地模型

1.2 项目依赖安装(适配 LangChain 1.0 + Ollama)

前两篇文章已做相关操作,可供参考: juejin.cn/post/757588…

1.3 安装LangSmith

官方文档:docs.langchain.com/langsmith/o…

二、核心步骤 1:LangChain 1.0 + Ollama 集成(标准化启动)

2.1 环境变量配置(.env)

所有配置通过环境变量隔离,适配不同环境(开发 / 生产):

# 服务基础配置
PORT=3000
NODE_ENV=development

# Ollama 配置
OLLAMA_BASE_URL=http://localhost:11434 #Ollama本地服务地址
OLLAMA_EMBED_MODEL=nomic-embed-text:latest #embedding模型
OLLAMA_CHAT_MODEL=deepseek-r1:8b #Thinking模型

# Chroma 向量数据库配置
CHROMA_URL=http://localhost:8000
CHROMA_COLLECTION_NAME=Devin-examples # 集合名称
CHROMA_PERSIST_DIR=./chroma-data # 存储地址

# LangSmith
LANGSMITH_API_KEY=xxx # LangSmith密钥
LANGSMITH_TRACING=true

2.2 LangChain + Ollama + LangSmith 初始化(核心配置)

import 'dotenv/config';
import type { AIMessageChunk, BaseMessage, MessageStructure } from '@langchain/core/messages';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
import { ChatOllama, type ChatOllamaInput } from '@langchain/ollama';
import { ragInstance } from './rag.js';
import { wrapSDK } from 'langsmith/wrappers';
/**
 * LLM 配置接口
 */
interface LLMConfig extends Partial<ChatOllamaInput> {
  /** 模型名称 */
  model?: string;
  /** 随机性参数,控制输出的随机性,范围 0-1,默认 0.7 */
  temperature?: number;
  /** Top-p 采样参数,控制多样性,范围 0-1 */
  topP?: number;
  /** 是否启用 RAG */
  enableRAG?: boolean;
}

/**
 * LLM 类
 * 提供流式和非流式的对话功能
 */
class LLM extends ChatOllama {
  private readonly enableRAG: boolean;
  /**
   * 构造函数
   * @param config LLM 配置选项
   */
  constructor(config: LLMConfig = {}) {
    const options: Record<string, unknown> = {
      model: config.model || process.env.OLLAMA_CHAT_MODEL,
      temperature: config.temperature ?? 0.7,
      think: config.think ?? true,
      ...config,
    };
    if (config.topP !== undefined) {
      options.topP = config.topP;
    }
    super(options);
    this.enableRAG = config.enableRAG ?? true;
  }

  /**
   * 非流式对话
   * @param message 用户消息
   * @param systemPrompt 系统提示词(可选)
   * @returns AI 完整回复文本
   */
  async chat(message: string, systemPrompt?: string): Promise<AIMessageChunk<MessageStructure>> {
    const messages: BaseMessage[] = await setMessage(message, this.enableRAG, systemPrompt);
    const response = await this.invoke(messages);
    return response;
  }

  /**
   * 流式对话
   * @param message 用户消息
   * @param systemPrompt 系统提示词(可选)
   * @returns 异步生成器,逐块返回内容
   */
  async *chatStream(message: string, systemPrompt?: string): AsyncGenerator<AIMessageChunk<MessageStructure>, void, unknown> {
    // 将message放入Chroma进行检索
    const messages: BaseMessage[] = await setMessage(message, this.enableRAG, systemPrompt);
    const stream = await this.stream(messages);
    for await (const chunk of stream) {
      yield chunk;
    }
  }
}
const setMessage = async (message: string, enableRAG: boolean, systemPrompt?: string): Promise<BaseMessage[]> => {
  const messages: BaseMessage[] = [];
  if (systemPrompt) {
    messages.push(new SystemMessage(systemPrompt));
  }
  // 是否启用RAG检索 后续模块会进行RAG的讲解
  if (enableRAG) {
    const ragMessage = await ragInstance.retrieve(message);
    messages.push(new SystemMessage(ragMessage));
  } else {
    messages.push(new HumanMessage(message));
  }
  return messages;
};
const llmInstance = (function () {
  let instance: LLM;
  return {
    getInstance: function () {
      if (!instance) {
        // 添加监控 通过LangSmith进行监控和可视化
        instance = wrapSDK(new LLM());
      }
      return instance;
    },
  };
})();

// 导出单例和类
export { LLM };
export default llmInstance;

2.3 Express 服务标准化启动

这部分就不进行详解,可自行查阅资料

这是我请求部分控制器的实现,可进行参考:

import type { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import llmInstance from '../utils/langchain/llm.js';
import { sendSSEData } from '../utils/http/sseTools.js';
import { badRequestResponse } from '../utils/http/http.js';
const llm = llmInstance.getInstance();
const sendSSEData = (res: Response, data: object, event: string = 'message'): void => {
  const arr = [`id: ${randomUUID()} \n`, `event: ${event}\n`, `data: ${JSON.stringify(data)}\n`];
  res.write(arr.join('') + '\n');
};
export const getLLMChart = async (req: Request, res: Response): Promise<void> => {
  try {
    const { text } = req.body;
    if (!text) {
      badRequestResponse(res);
      return;
    }
    // 发送连接成功消息
    sendSSEData(res, { type: 'start' });

    // 使用流式响应
    const stream = llm.chatStream(text);

    for await (const chunk of stream) {
      // 按照 SSE 格式发送数据
      const data = {
        id: randomUUID(),
        type: 'message',
        message: chunk,
        timestamp: new Date().toISOString(),
      };
      sendSSEData(res, data);
    }
    // 发送完成消息
    sendSSEData(res, { type: 'end' });
    res.end();
  } catch (error) {
    // SSE 错误处理
    const errorData = {
      status: 'error',
      message: error instanceof Error ? error.message : 'Unknown error',
    };
    res.write(`data: ${JSON.stringify(errorData)}\n\n`);
    res.end();
  }
};

三.接口调用、LangSmith监控

image.png

image.png

从上图可以明确的看到LLM 应用调试时间、请求成功与否、数据格式等等