最近在开发仿真平台的Agent,其场景和开发思路和大多数低代码平台都是一样的。我将Agent功能分为两大块:文档助手和智能生成。
本文介绍文档助手agent的开发。
文档助手
低码类平台常常有非常多的配置,也具有复杂的操作手册和各式案例,是智能文档助手的最佳应用场景。
技术选型
项目中rag部分的关键技术选型是chromadb。
ts
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是没有办法运行这种多模态模型的。)
typescript
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的标准流程是载入文档 → 分词 → 嵌入 → 向量存储 → 检索。
载入文档
typescript
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)。
其代码实现很简单:
typescript
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);
}
这里简单介绍下它的原理------分级降级,尽力而为
- 按优先级切分 :它首先会尝试用一个较高的分隔符(比如段落分隔符
\n\n)来切分文本,希望保持段落完整。 - 检查块大小 :如果切分出来的某个块仍然超过了预设的
chunk_size,它不会强行保留这个超长的块,而是进入下一步。 - 递归降级处理 :如果高优先级的分隔符切出的块过长,它会自动"降级",对这个过长的块使用优先级次之的分隔符(比如句号
.或。)再次尝试切分。 - 循环直至完成 :这个过程会一直递归下去,直到所有的块都符合大小要求。如果到最后使用最小分隔符(如单个字符
"")切分后块还是过大,它最终也只能产生一个超长块,并给出警告。
基于以上原理,我们可以额外添加中文标点
typescript
export function createSplitter(): RecursiveCharacterTextSplitter {
return new RecursiveCharacterTextSplitter({
separators: [
"\n\n", // 段落
"\n", // 换行
"。",
"!",
"?",
";", // 中文句子结束符
",",
"、", // 中文逗号/顿号
" ", // 空格
"", // 字符
],
chunkSize: CHUNK_SIZE,
chunkOverlap: CHUNK_OVERLAP,
});
}
嵌入和存储
Chroma.from_documents() 方法是一个将向量化(Embedding) 和存储(Storage) 两个核心步骤合二为一的便捷方法。 调用它时,会在内部自动执行以下步骤:
- 调用嵌入模型 :它会调用你通过
embedding参数传入的嵌入模型,将documents列表中的每一个Document的文本内容转换成对应的向量(Embeddings)。 - 存储到数据库 :它会创建一个 Chroma 数据库的连接,并将上一步生成的 向量 与对应的 文档原文、元数据 一起,存储到 Chroma 中,形成一个完整的向量数据库。
typescript
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会自动完成这一步。
ts
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);