最近在开发仿真平台的Agent,其场景和开发思路和大多数低代码平台都是一样的。我将Agent功能分为两大块:文档助手和智能生成。

本文介绍文档助手agent的开发。

文档助手

低码类平台常常有非常多的配置,也具有复杂的操作手册和各式案例,是智能文档助手的最佳应用场景。

技术选型

项目中rag部分的关键技术选型是chromadb。

import { Chroma } from "@langchain/community/vectorstores/chroma";

function getVectorStore(): Promise<Chroma> {
  if (vectorStore) {
    return vectorStore;
  }

  vectorStore = new Chroma(embeddings, {
    collectionName: COLLECTION_NAME,
    url: `http://${config.chromaHost}:${config.chromaPort}`,
  });

  return vectorStore;
}

chromadb有内置的embedding模型(all-MiniLM-L6-v2),不过我选择Qwen3-Embedding-8B,该处理中文更好。

另外如果知识库中有大量图片的,建议使用Qwen3-VL-Embedding-8B。(这个模型跑起来比较难,要么调阿里云的api,要么就得找个好点的机子本地部署。ollama是没有办法运行这种多模态模型的。)

import { OpenAIEmbeddings } from "@langchain/openai";
import { config } from "../config/index.js";

export const embeddings = new OpenAIEmbeddings({
  model: config.embeddingModel,
  batchSize: 8,
  timeout: 300_000,
  configuration: {
    baseURL: config.embeddingBaseUrl,
  },
});

rag流程

rag的标准流程是载入文档 → 分词 → 嵌入 → 向量存储 → 检索

载入文档

function loadDocuments(): Promise<Document[]> {
  const files = await getFiles(DOCS_PATH);
  const documents: Document[] = [];

  for (const file of files) {
    const rawContent = await readFile(file, 'utf-8');


    //如果不是纯文本,比如excel/html,需要将其转为纯文本
    const text = convert(rawContent);

    documents.push(
      new Document({
        pageContent: text,
        metadata: {
          source: file,
          title,
        },
      })
    );
  }

  return documents;
}

分词

分词工具建议使用RecursiveCharacterTextSplitter(@langchain/textsplitters)。

其代码实现很简单:

import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
import type { Document } from '@langchain/core/documents';

const CHUNK_SIZE = 1000;
const CHUNK_OVERLAP = 200;

export function createSplitter(): RecursiveCharacterTextSplitter {
  return new RecursiveCharacterTextSplitter({
    chunkSize: CHUNK_SIZE,
    chunkOverlap: CHUNK_OVERLAP,
  });
}

export async function splitDocuments(docs: Document[]): Promise<Document[]> {
  const splitter = createSplitter();
  return splitter.splitDocuments(docs);
}

这里简单介绍下它的原理——分级降级,尽力而为

  1. 按优先级切分:它首先会尝试用一个较高的分隔符(比如段落分隔符 \n\n)来切分文本,希望保持段落完整。
  2. 检查块大小:如果切分出来的某个块仍然超过了预设的 chunk_size,它不会强行保留这个超长的块,而是进入下一步。
  3. 递归降级处理:如果高优先级的分隔符切出的块过长,它会自动“降级”,对这个过长的块使用优先级次之的分隔符(比如句号 . 或 )再次尝试切分。
  4. 循环直至完成:这个过程会一直递归下去,直到所有的块都符合大小要求。如果到最后使用最小分隔符(如单个字符 "")切分后块还是过大,它最终也只能产生一个超长块,并给出警告

基于以上原理,我们可以额外添加中文标点

export function createSplitter(): RecursiveCharacterTextSplitter {
  return new RecursiveCharacterTextSplitter({
    separators: [
      "\n\n", // 段落
      "\n", // 换行
      "。",
      "!",
      "?",
      ";", // 中文句子结束符
      ",",
      "、", // 中文逗号/顿号
      " ", // 空格
      "", // 字符
    ],
    chunkSize: CHUNK_SIZE,
    chunkOverlap: CHUNK_OVERLAP,
  });
}

嵌入和存储

Chroma.from_documents() 方法是一个将向量化(Embedding)  和存储(Storage)  两个核心步骤合二为一的便捷方法。 调用它时,会在内部自动执行以下步骤:

  1. 调用嵌入模型:它会调用你通过 embedding 参数传入的嵌入模型,将 documents 列表中的每一个 Document 的文本内容转换成对应的向量(Embeddings)。
  2. 存储到数据库:它会创建一个 Chroma 数据库的连接,并将上一步生成的 向量 与对应的 文档原文、元数据 一起,存储到 Chroma 中,形成一个完整的向量数据库。
 const store = await Chroma.fromDocuments(docs, embeddings, {
    collectionName: COLLECTION_NAME,
    url: `http://${config.chromaHost}:${config.chromaPort}`,
  }).catch((error) => {
    console.error("Error creating vector store:", error);
    throw error;
  });

这一步根据材料和模型的不同,耗时可能会很长。文档更新,最好能够增量embedding。

查询

查询时也需要将原始字符串转为向量才行,不过similaritySearch会自动完成这一步。

   const llm = createLLM(0.3);

  const docs: Document[] = await vectorStore.similaritySearch(query, 5);
  const contextDocs: string[] = docs.map((doc: Document) => doc.pageContent);
  // 源文档,可以用作连接
  const sources: string[] = [
    ...new Set(docs.map((doc: Document) => doc.metadata.source as string)),
  ];
  // 向量数据库查找出来的内容塞到llm的promot中 
  const prompt = buildPrompt(
    query,
    contextDocs,
    projectContext,
    conversationHistory,
  );

  const response = await llm.invoke(prompt);