最近在搭建仿真平台的智能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: `https://${config.chromaHost}:${config.chromaPort}`,
});
return vectorStore;
}
ChromaDB 虽然内置了嵌入模型(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);
}
简单说明一下它的核心原理,本质上是“分级降级,尽力而为”:
- 按优先级切分:先用较高的分隔符(比如段落分隔符
\n\n)尝试切分,尽量维持段落完整性。 - 检查块大小:如果某个切分后的块仍然超过预设的
chunk_size,并不会硬性截断,而是继续向下处理。 - 递归降级处理:对超长的块自动降级,采用更低优先级的分隔符(如句号
.或。)再次切分。 - 循环直至完成:不断递归,直到所有块都符合大小要求。如果最终使用最小分隔符(单个字符
"")仍超长,则只能生成一个超长块,并抛出警告。
基于这一原理,我们还可以额外加入一些中文标点作为分隔符:
export function createSplitter(): RecursiveCharacterTextSplitter {
return new RecursiveCharacterTextSplitter({
separators: [
"\n\n", // 段落分隔
"\n", // 换行符
"。",
"!",
"?",
";", // 中文句子结束符
",",
"、", // 中文逗号/顿号
" ", // 空格
"", // 字符级别
],
chunkSize: CHUNK_SIZE,
chunkOverlap: CHUNK_OVERLAP,
});
}
向量嵌入与存储
Chroma.from_documents() 是一个将向量化和存储两步合并的便捷方法。调用时,内部会自动执行:
- 调用嵌入模型:对传入的每个
Document的文本内容转换为向量。 - 存入数据库:建立 Chroma 连接,将向量连同原始文本和元数据一起存储,形成完整的向量数据库。
const store = await Chroma.fromDocuments(docs, embeddings, {
collectionName: COLLECTION_NAME,
url: `https://${config.chromaHost}:${config.chromaPort}`,
}).catch((error) => {
console.error("向量存储创建失败:", error);
throw error;
});
这一步的耗时取决于文档数量和嵌入模型,可能很长。当文档更新时,最好能支持增量式的嵌入更新。
查询检索
查询时同样需要将原始字符串转换为向量,不过 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的提示词中
const prompt = buildPrompt(
query,
contextDocs,
projectContext,
conversationHistory
);
const response = await llm.invoke(prompt);
```