主题
项目线 D | 全栈自托管 AI Agent — 零 API 依赖的企业知识问答系统
这条项目线是项目线 A 的自托管平行版本。如果说 A 是"用云端 API 快速跑通完整链路",D 就是"把同一套架构搬到你自己可控的环境里,做到数据不出域、零外部 API 调用"。两条线形成云端/本地双版本对照,既能独立展示,也能合在一起讲一个更完整的技术选型故事。
1. 项目定位
1.1 项目目标
构建一个完全运行在本地或私有环境的知识问答 Agent。核心价值不是"能跑起来",而是:
- 零 API 费用:推理、Embedding、检索全链路本地运行,边际成本接近零。
- 数据完全不出域:所有文档内容、用户查询、模型推理都不经过任何第三方网络。
- 可在内网离线运行:断网也不影响核心功能。
1.2 与项目线 A 的关系
项目线 A 使用 OpenAI GPT-4o + 云端 Embedding + 云端向量库构建知识问答系统。本项目用开源模型 + 本地 Embedding + 本地向量库实现相同功能。业务逻辑层几乎一样,差异全在基础设施层。
这种"相同架构 + 不同基础设施"的对照关系,能帮助你深入理解:哪些能力是 Agent 的核心逻辑,哪些只是外部服务的接入方式。它也是面试中一个很强的叙事结构——"我做过云端版本,也做过自托管版本,我知道它们的 trade-off。"
1.3 适合场景
- 金融合规:客户数据和内部文档不能离开内网。
- 医疗数据保护:患者信息和临床指南需要严格的数据边界。
- 政务内网:物理隔离环境中需要智能问答能力。
- 个人知识库:不想把笔记和私人文档发给第三方 API。
2. 项目边界
2.1 包含范围
- ✅ 本地模型部署与推理(Ollama + Qwen3-8B)。
- ✅ 本地 Embedding 生成(BGE-M3)。
- ✅ 本地向量数据库(ChromaDB)。
- ✅ 完整 RAG 管道(文档处理 → 分块 → Embedding → 检索 → 生成)。
- ✅ Tool Calling 集成。
- ✅ 前端 UI(Next.js + Vercel AI SDK)。
- ✅ Docker Compose 一键部署。
2.2 不含范围
- ⚠️ 模型微调:本项目不包含,但完成后可参考 专题 12 模型微调入门 进一步定制模型。
- ❌ 分布式推理和多节点调度。
- ❌ GPU 集群管理与编排。
2.3 前置要求
- 已安装 Ollama(参考专题 11 第 2.1 节)。
- 硬件满足以下最低条件之一:
- NVIDIA GPU:8GB+ 显存(RTX 3060 及以上)。
- Apple Silicon Mac:16GB+ 统一内存(M2/M3/M4 均可)。
- 纯 CPU 环境:可运行但推理速度较慢,仅建议用于验证功能。
3. 技术栈
| 层级 | 云端版(项目 A) | 自托管版(本项目) |
|---|---|---|
| 推理 | OpenAI GPT-4o | Ollama + Qwen3-8B |
| Embedding | OpenAI text-embedding-3 | BGE-M3 via Ollama |
| 向量库 | Pinecone / Weaviate | ChromaDB |
| 后端 | Next.js API Routes | Next.js API Routes |
| 前端 | React + Vercel AI SDK | React + Vercel AI SDK |
| 部署 | Vercel + 云服务 | Docker Compose 本地 |
后端和前端层完全相同,这不是巧合——它是刻意的架构设计。通过 OpenAI 兼容 API 和环境变量切换,业务代码在两个版本之间零修改复用。这个设计思路在专题 11 第 8 节有详细说明。
4. 能力地图
本项目覆盖以下主线章节的实践能力:
| 能力 | 对应章节 | 本项目实践 |
|---|---|---|
| 上下文工程 | 专题 1 | RAG 检索上下文组装、系统提示词设计 |
| 模型选型与调用 | 专题 11 第 3 节 | Qwen3-8B 选型、量化方案决策 |
| 本地推理引擎 | 专题 11 第 2 节 | Ollama 部署与 OpenAI 兼容 API |
| Embedding 自托管 | 专题 11 第 6 节 | BGE-M3 本地 Embedding 管道 |
| 向量数据库 | 专题 11 第 7 节 | ChromaDB 存储与检索 |
| Tool Calling | 专题 11 第 11 节 Step 6 | 本地模型工具调用与格式处理 |
| Agent UI | 专题 4 | 流式输出、模型状态面板、引用展示 |
| 可观测性 | 专题 5 | 健康检查、推理延迟监控 |
| 成本与性能 | 专题 6 | 零 API 成本、量化降级策略 |
| 前端工具链 | 专题 9 | Next.js + Vercel AI SDK 集成 |
5. 系统架构
整个系统由五个组件组成,通过 Docker 内部网络通信:
用户浏览器
│
▼
┌─────────────────────────────────────────┐
│ Next.js 应用(前端 + API Routes) │ :3000
│ ├─ 前端 UI:流式对话、引用展示、状态面板 │
│ └─ API Routes:RAG 编排、Tool Calling │
└────────────┬──────────────┬──────────────┘
│ │
推理请求 │ │ Embedding / 检索
▼ ▼
┌────────────┐ ┌────────────┐
│ Ollama │ │ ChromaDB │
│ Qwen3:8b │ │ 向量存储 │
│ BGE-M3 │ │ │
│ :11434 │ │ :8000 │
└────────────┘ └────────────┘数据流:
- 用户在前端输入问题。
- API Route 调用 Ollama BGE-M3 生成问题的 Embedding。
- 用 Embedding 在 ChromaDB 中检索相关文档片段。
- 将检索结果作为上下文,连同用户问题一起发给 Ollama Qwen3:8b。
- 模型生成回答,通过流式响应返回前端。
- 前端逐 token 渲染回答,展示引用来源。
6. 前端工程师的核心职责
本项目不是"后端搞定了,前端接个接口"。前端工程师在以下环节承担关键角色:
6.1 Ollama 服务集成与健康检查
在应用启动时和运行中,需要持续检查 Ollama 服务和模型状态。这不同于云端 API——本地服务可能未启动、模型可能未拉取、GPU 可能被其他进程占用。
6.2 流式响应对接
Ollama 的流式响应格式与 OpenAI 略有差异。通过 Vercel AI SDK 的 createOpenAI 适配器可以抹平差异,但需要注意本地模型的首 token 延迟通常更高(模型需要加载到显存),前端需要设计更好的加载状态来应对这个差异。
6.3 Embedding 管道编排
文档导入时需要调用本地 Embedding 模型逐条生成向量,这个过程比云端 API 慢(没有批量并发优势),前端需要展示导入进度和错误处理。
6.4 前端 UI 特有需求
除了标准的聊天界面,自托管版本还需要:
- 模型状态显示:当前加载的模型、显存占用、推理速度。
- 本地/云端切换开关:一键切换推理后端,方便对比效果。
- 服务健康面板:Ollama、ChromaDB 的连接状态。
6.5 Docker Compose 编排与一键部署
前端工程师需要理解 Docker Compose 的服务编排,确保前端应用能正确连接到 Ollama 和 ChromaDB 容器。部署脚本和健康检查也是前端工程师的交付物之一。
7. 分阶段实施计划
Phase 1:环境搭建(1-2 天)
目标:所有基础服务运行正常。
- 安装 Ollama,拉取 Qwen3:8b 和 BGE-M3 模型。
- Docker 启动 ChromaDB。
- 验证各服务健康状态。
验证脚本:
typescript
async function checkHealth() {
const checks = {
ollama: false,
model: false,
embedding: false,
chromadb: false,
};
// Ollama 服务
try {
const res = await fetch('http://localhost:11434/api/tags');
if (res.ok) {
checks.ollama = true;
const { models } = await res.json();
checks.model = models.some((m: { name: string }) => m.name.includes('qwen3'));
checks.embedding = models.some((m: { name: string }) => m.name.includes('bge-m3'));
}
} catch {
console.error('Ollama 服务未启动');
}
// ChromaDB
try {
const res = await fetch('http://localhost:8000/api/v1/heartbeat');
checks.chromadb = res.ok;
} catch {
console.error('ChromaDB 未启动');
}
console.table(checks);
const allPassed = Object.values(checks).every(Boolean);
console.log(allPassed ? '✅ 所有服务就绪' : '❌ 部分服务未就绪');
return checks;
}Phase 2:RAG 管道(2-3 天)
目标:文档导入、Embedding 生成、向量检索全链路跑通。
- 文档读取与分块——按标题和段落分割,保留元数据。
- 本地 Embedding 生成——调用 Ollama BGE-M3 逐条生成向量。
- 向量存储——写入 ChromaDB。
- 检索测试——验证 top-K 召回质量。
核心代码参考专题 11 第 7.3 节的完整 RAG 链路示例。
Phase 3:Agent 核心(2-3 天)
目标:基于本地模型的对话、Tool Calling、上下文管理全部可用。
- 对话链路——通过 OpenAI 兼容 API 接入 Qwen3:8b。
- Tool Calling——定义工具 schema,处理模型返回的工具调用请求。
- 上下文管理——历史消息裁剪、系统提示词优化。
本地模型的 Tool Calling 稳定性不如云端模型,需要增加格式校验和重试逻辑。具体处理方式见第 10 节。
Phase 4:前端集成(1-2 天)
目标:完整的交互界面上线。
- Next.js + Vercel AI SDK 对接 Ollama。
- 流式 UI 与 Markdown 渲染。
- 模型状态监控面板。
- 引用来源展示。
Phase 5:部署与优化(1 天)
目标:一键部署、性能可接受。
- Docker Compose 配置。
- 性能调优:量化方案选择、
num_ctx上下文窗口调整、num_gpu层数分配。 - 部署文档编写。
8. 关键代码片段
8.1 Ollama 连接配置——环境变量控制云端/本地切换
这是整个项目最关键的设计:通过环境变量切换推理后端,业务代码无需任何修改。
typescript
import { createOpenAI } from '@ai-sdk/openai';
function createModelProvider() {
const isLocal = process.env.AI_PROVIDER === 'local';
return createOpenAI({
baseURL: isLocal
? process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434/v1'
: 'https://api.openai.com/v1',
apiKey: isLocal ? 'ollama' : process.env.OPENAI_API_KEY!,
});
}
export const ai = createModelProvider();
export const MODEL_NAME = process.env.AI_PROVIDER === 'local'
? 'qwen3:8b'
: 'gpt-4o';bash
# .env.local — 本地模式
AI_PROVIDER=local
OLLAMA_BASE_URL=http://localhost:11434/v1
# .env.production — 云端模式
AI_PROVIDER=cloud
OPENAI_API_KEY=sk-xxx8.2 本地 Embedding 生成 + ChromaDB 存储
typescript
import { ChromaClient } from 'chromadb';
const chroma = new ChromaClient({
path: process.env.CHROMA_URL ?? 'http://localhost:8000',
});
async function embed(text: string): Promise<number[]> {
const res = await fetch(
`${process.env.OLLAMA_BASE_URL?.replace('/v1', '') ?? 'http://localhost:11434'}/api/embeddings`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'bge-m3', prompt: text }),
},
);
const data = await res.json();
return data.embedding;
}
interface DocChunk {
id: string;
text: string;
metadata: { source: string; title: string };
}
async function indexDocuments(docs: DocChunk[]) {
const collection = await chroma.getOrCreateCollection({ name: 'knowledge' });
for (const doc of docs) {
const embedding = await embed(doc.text);
await collection.add({
ids: [doc.id],
documents: [doc.text],
embeddings: [embedding],
metadatas: [doc.metadata],
});
}
console.log(`已索引 ${docs.length} 条文档`);
}8.3 RAG 检索 + 本地模型生成
typescript
import { streamText } from 'ai';
import { ai, MODEL_NAME } from './config';
async function retrieve(query: string, topK = 3) {
const queryEmbedding = await embed(query);
const collection = await chroma.getCollection({ name: 'knowledge' });
const results = await collection.query({
queryEmbeddings: [queryEmbedding],
nResults: topK,
});
return {
documents: results.documents?.[0] ?? [],
metadatas: results.metadatas?.[0] ?? [],
};
}
export async function POST(req: Request) {
const { messages } = await req.json();
const lastMessage = messages[messages.length - 1].content;
const { documents, metadatas } = await retrieve(lastMessage);
const context = documents
.map((doc, i) => `[来源: ${(metadatas[i] as { title?: string })?.title ?? '未知'}]\n${doc}`)
.join('\n---\n');
const result = streamText({
model: ai(MODEL_NAME),
system: `你是一个企业知识问答助手。基于以下参考资料回答问题,每个回答必须注明引用来源。如果资料不足以回答,明确说明"根据现有资料无法确认"。
参考资料:
${context}`,
messages,
});
return result.toDataStreamResponse();
}8.4 前端流式 UI + 模型状态面板
typescript
'use client';
import { useChat } from '@ai-sdk/react';
import { useEffect, useState } from 'react';
interface ServiceStatus {
ollama: boolean;
model: boolean;
embedding: boolean;
chromadb: boolean;
}
function useServiceHealth() {
const [status, setStatus] = useState<ServiceStatus>({
ollama: false, model: false, embedding: false, chromadb: false,
});
useEffect(() => {
async function check() {
const res = await fetch('/api/health');
if (res.ok) setStatus(await res.json());
}
check();
const id = setInterval(check, 30_000);
return () => clearInterval(id);
}, []);
return status;
}
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
const health = useServiceHealth();
const allHealthy = Object.values(health).every(Boolean);
return (
<div style={{ maxWidth: 720, margin: '0 auto', padding: 20 }}>
<header style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<h1>自托管知识问答</h1>
<span style={{ color: allHealthy ? 'green' : 'red' }}>
{allHealthy ? '● 所有服务正常' : '○ 部分服务异常'}
</span>
</header>
<div>
{messages.map(m => (
<div key={m.id} style={{ margin: '12px 0' }}>
<strong>{m.role === 'user' ? '你' : 'AI'}:</strong>
<span>{m.content}</span>
</div>
))}
{isLoading && <div style={{ color: '#888' }}>思考中...</div>}
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: 8, marginTop: 16 }}>
<input
value={input}
onChange={handleInputChange}
placeholder={allHealthy ? '输入问题...' : '服务未就绪,请检查状态'}
disabled={isLoading || !allHealthy}
style={{ flex: 1, padding: 8 }}
/>
<button type="submit" disabled={isLoading || !allHealthy}>
发送
</button>
</form>
</div>
);
}9. 验收指标
| 指标 | 阈值 | 测量方式 |
|---|---|---|
| 首次响应延迟 | < 2s(8B Q4 模型,GPU 推理) | 从请求发出到首 token 返回的时间 |
| 知识问答准确率 | > 80%(20 题测试集) | 人工标注 + 引用匹配 |
| Docker Compose 启动时间 | < 5 分钟(不含模型下载) | docker compose up 到所有健康检查通过 |
| 全链路零外部 API 调用 | 100% | 网络抓包验证无外发请求 |
| Embedding 单条延迟 | < 200ms(GPU) | Ollama API 响应时间 |
| 流式输出 token 速率 | > 15 tokens/s(8B Q4,GPU) | 生成 200 token 回答的平均速率 |
10. 常见坑与解决方案
10.1 Ollama 模型下载慢
默认从海外源下载,几 GB 的模型文件在国内网络下容易超时。
解决:设置 Ollama 镜像源,或从 HuggingFace 镜像站(hf-mirror.com)手动下载 GGUF 文件后通过 Modelfile 本地导入。详细步骤见专题 11 第 5.4 节。
10.2 显存不足 OOM
8B 模型的 FP16 需要约 16GB 显存,超出 8GB 显卡的容量。
解决:使用 Q4_K_M 量化版本(约 4-5GB 显存),或降级到更小的模型。Apple Silicon 可以利用统一内存运行更大的模型。量化选型参考专题 11 第 5.3 节的量化级别对比表。
10.3 Tool Calling 解析错误
本地模型(尤其是小参数量模型)在 Tool Calling 的格式稳定性上不如 GPT-4o。可能出现 JSON 格式错误、参数缺失等问题。
解决:
- 选用 Qwen3 或 Mistral 等 Tool Calling 支持成熟的模型。
- 在 Prompt 中给出清晰的工具调用示例。
- 增加输出校验:尝试
JSON.parse,失败则重试。 - 设置重试上限(建议 2-3 次),超限则 fallback 到直接回答。
10.4 ChromaDB 连接超时
Docker Compose 环境中,应用容器可能在 ChromaDB 完全启动前就尝试连接。
解决:在 Docker Compose 中使用 depends_on 配合健康检查,或在应用启动逻辑中加入连接重试:
typescript
async function waitForChroma(maxRetries = 10, interval = 2000) {
for (let i = 0; i < maxRetries; i++) {
try {
const res = await fetch(`${process.env.CHROMA_URL}/api/v1/heartbeat`);
if (res.ok) return;
} catch {
// 连接失败,等待后重试
}
await new Promise(r => setTimeout(r, interval));
}
throw new Error('ChromaDB 连接超时');
}10.5 本地模型回答质量不如云端
8B 模型在复杂推理和长文本理解上确实不如 GPT-4o。
解决:通过 RAG 增强上下文弥补模型能力差距;通过更精确的 Prompt 约束输出格式;对少数关键场景保留 fallback 到云端的能力(通过第 8.1 节的环境变量切换实现)。
11. 与项目线 A 的对照学习建议
11.1 推荐学习路径
建议先完成项目线 A 的云端版本,再做本项目。原因:
- 云端版本上手更快,能让你先理解 RAG 和 Agent 的核心逻辑。
- 自托管版本有额外的环境搭建和调优成本,提前熟悉业务逻辑能让你专注于基础设施差异。
11.2 重点体会的差异
| 维度 | 云端版本(项目 A) | 自托管版本(本项目) |
|---|---|---|
| 启动速度 | 注册 API Key 即可开始 | 需要安装 Ollama、拉取模型、启动 Docker |
| 推理质量 | GPT-4o 顶级能力 | 8B 模型有差距,需要 RAG + Prompt 补偿 |
| 首 token 延迟 | ~200ms(网络延迟为主) | ~500ms-1s(模型加载和推理为主) |
| 运行成本 | 按 token 计费,持续增长 | 硬件一次性投入,边际成本接近零 |
| 数据隐私 | 数据经过 OpenAI 服务器 | 数据完全本地 |
| Tool Calling 稳定性 | 高 | 中等,需要格式校验和重试 |
| 运维负担 | 零(API 服务商负责) | 需要管理 Ollama 服务、模型版本、GPU 资源 |
11.3 输出建议
完成两个版本后,建议撰写一份 trade-off 分析文档,重点覆盖:
- 成本模型对比(按月请求量估算)。
- 质量差距量化(同一测试集的准确率对比)。
- 适用场景总结(什么时候选云端、什么时候选本地、什么时候混合部署)。
这份文档本身就是一个很好的面试材料。
12. 面试表达模板
自建经验介绍
"我独立搭建过完全自托管的知识问答 Agent,使用 Ollama + Qwen3 + BGE-M3 + ChromaDB,全链路零外部 API 调用,数据完全不出域。系统通过 Docker Compose 一键部署,在断网环境下也能正常运行。"
与云端版本的对比
"相比云端版本,自托管版本在数据隐私和长期成本上有明显优势。但本地 8B 模型在复杂推理和 Tool Calling 稳定性上不如 GPT-4o,我通过 RAG 增强上下文、Prompt 约束输出格式、增加格式校验和重试机制来弥补这些差距。"
架构设计
"我在设计时保持了 baseURL 可配置的架构,业务代码在云端和本地版本之间零修改复用。这样团队可以在开发阶段用云端 API 快速验证,生产环境切到本地模型满足合规要求。这个设计让我们的迁移成本几乎为零。"
诚实说明局限
"本地模型和云端模型之间确实有能力差距,尤其在复杂推理场景。但在我们的业务场景里,80% 的知识问答用 8B 模型加 RAG 就够了。剩下 20% 需要强推理的场景,系统支持按需 fallback 到云端 API。"
13. 阶段性交付物清单
| 阶段 | 交付物 | 验收标准 |
|---|---|---|
| Phase 1 | 环境搭建文档 + 健康检查脚本 | 脚本输出全部 ✅ |
| Phase 2 | RAG 管道代码 + 检索测试结果 | 20 条测试查询的 top-3 召回率 > 70% |
| Phase 3 | Agent 核心代码 + Tool Calling 演示 | 工具调用成功率 > 80% |
| Phase 4 | 前端应用 + 录屏演示 | 流式输出正常、状态面板可用 |
| Phase 5 | Docker Compose 配置 + 部署文档 | 新机器一键部署 5 分钟内完成 |
14. 验收清单
- [ ] Ollama 本地运行 Qwen3 模型,API 响应正常。
- [ ] BGE-M3 本地 Embedding 正常工作,单条延迟 < 200ms。
- [ ] ChromaDB 存储与检索功能正常,支持 top-K 查询。
- [ ] RAG 全链路零外部 API 调用(网络抓包验证)。
- [ ] 前端流式输出正常,首 token 延迟 < 2s。
- [ ] 模型状态监控面板可用,展示服务健康状态。
- [ ] 本地/云端切换通过环境变量实现,业务代码无需修改。
- [ ] Docker Compose 一键部署成功,所有服务自动启动。
- [ ] 20 题测试集准确率 > 80%。
- [ ] 与云端版本的 trade-off 分析文档完成。