主题
专题 11 | 自托管 AI Agent 全栈指南 — 摆脱第三方依赖,构建自有智能体
本专题面向"会用 OpenAI API 构建 Agent,但被 API 费用、数据合规或网络限制困住"的前端工程师。它要解决的核心问题不是"本地模型怎么装",而是怎么把一套完整的 Agent 系统——推理、嵌入、检索、工具调用、前端交互——全部搬到自己可控的环境中,同时保持与云端 API 代码的兼容性。
1. 为什么要自建
1.1 四大驱动力
自建 AI Agent 不是技术极客的玩具,而是越来越多团队的工程选择。驱动力来自四个方向:
- 成本控制:当你的 Agent 每天处理上万次请求时,云端 API 的 token 费用会迅速增长。本地推理的边际成本接近零——电费和硬件折旧远低于按量计费。
- 数据隐私与合规:金融、医疗、政务等行业明确要求数据不出域。即使云端 API 承诺不存储数据,审计时"数据经过第三方网络"本身就是合规风险。
- 离线与内网场景:工厂产线、军事系统、偏远地区部署——没有互联网,就没有云端 API。但本地部署的模型照跑不误。
- 深度定制:云端模型是通用的,你无法修改它的推理逻辑、微调它的行为,或在系统提示被忽略时调整底层权重。自建意味着从模型选择到推理参数的完全控制权。
前端迁移提示:这和"自建 CDN vs 用 Cloudflare"的决策非常像——Cloudflare 上手快、省心,但当流量大到一定规模、或有特殊合规要求时,自建就开始划算了。
1.2 自建 vs 云端 API 的 trade-off 决策矩阵
| 维度 | 云端 API(OpenAI/Claude) | 自托管(Ollama/vLLM) |
|---|---|---|
| 上手速度 | 注册即用,分钟级 | 需安装环境,小时级 |
| 推理质量 | 顶级模型,持续迭代 | 开源模型有差距,但快速追赶 |
| 单次成本 | 按 token 计费,随用随付 | 硬件一次性投入,边际成本低 |
| 大规模成本 | 线性增长,可能很贵 | 固定成本,规模越大越划算 |
| 数据隐私 | 数据经过第三方 | 数据完全本地 |
| 网络依赖 | 必须联网 | 可离线运行 |
| 定制空间 | 有限(Prompt/微调 API) | 完全控制(模型/参数/部署) |
| 运维负担 | 零运维 | 需自行管理 GPU、更新、监控 |
决策建议:不是二选一。很多生产系统采用混合方案——开发阶段用云端 API 快速验证,生产环境逐步迁移到本地模型。本专题的代码设计会保持这种灵活性。
2. 本地推理引擎对比
推理引擎是自托管 Agent 的核心基础设施,决定了模型怎么加载、怎么调用、性能上限在哪。
2.1 Ollama
定位是"本地模型的 Docker"——一条命令拉取模型,自动处理格式转换和硬件适配。对 Apple Silicon 做了专门优化,M 系列芯片可以直接利用统一内存跑大模型。内置 REST API,启动后就是一个 HTTP 服务。
适合场景:个人开发、快速原型、中小团队内部部署。
2.2 vLLM
面向生产级高并发场景的推理引擎。核心技术是 PagedAttention,大幅优化了多请求并发时的显存管理,吞吐量远高于朴素实现。原生提供 OpenAI 兼容 API,可以直接替换 baseURL。
适合场景:多用户并发的生产服务、需要高吞吐的 API 网关。
2.3 llama.cpp
纯 C/C++ 实现的推理库,目标是在尽可能多的硬件上高效运行 LLM。支持 CPU 推理、各类 GPU 加速,是 GGUF 模型格式的定义者。极致轻量,可以编译到嵌入式设备。
适合场景:边缘部署、资源极度受限的环境、需要最底层控制的团队。
2.4 LocalAI
兼容 OpenAI API 的本地推理网关,支持同时管理多个模型(文本、图像、音频、Embedding),提供统一接口。底层可以调用 llama.cpp、Whisper 等多种引擎。
适合场景:需要一个入口管理多种本地模型的团队。
2.5 对比表格
| 维度 | Ollama | vLLM | llama.cpp | LocalAI |
|---|---|---|---|---|
| 安装难度 | 极低(一键安装) | 中等(pip) | 较高(需编译) | 中等(Docker) |
| 并发能力 | 中等 | 极高 | 低(单请求优化) | 中等 |
| GPU 要求 | 可选(支持 CPU) | 推荐 NVIDIA | 可选 | 可选 |
| Apple Silicon | 原生优化 | 有限支持 | 良好支持 | 有限支持 |
| OpenAI 兼容 | 是 | 是 | 需额外服务 | 是 |
| 适合阶段 | 开发/小规模生产 | 大规模生产 | 边缘/嵌入式 | 多模型管理 |
前端迁移提示:Ollama 的 REST API 和你熟悉的 Express 接口没什么区别——
POST /api/chat发 JSON、收 JSON,支持流式响应。如果你能写fetch,你就能调本地模型。
3. 开源模型选型矩阵
开源模型迭代极快,每隔几个月就有新的强力选手。与其记住具体版本号,不如建立选型思维框架。
3.1 主流模型系列
- Qwen 系列:中文能力突出,Apache 2.0 许可,Tool Calling 支持成熟,从 0.6B 到 200B+ 覆盖各种规模。中文场景首选。
- Llama 系列:Meta 出品,社区生态最大,英文能力强,第三方工具支持最完善。许可证需注意商用条款。
- Mistral/Mixtral 系列:欧洲团队出品,Tool Calling 和结构化输出能力稳定,MoE 架构在同等参数量下推理更快。
- DeepSeek 开源版:推理和代码能力强,MoE 架构效率高,中文支持好。
- Phi 系列:微软出品的小模型系列,在同参数量级性能领先,适合端侧部署。
3.2 选型维度表格
| 维度 | 评估要点 |
|---|---|
| 参数量 | 7-8B 适合入门和端侧;14B 是性价比甜点;70B+ 接近云端质量 |
| 中文能力 | Qwen、DeepSeek 领先,Llama 需要中文微调版 |
| Tool Calling | Qwen、Mistral 原生支持好,部分模型需要特定 Prompt 格式 |
| 许可证 | Apache 2.0 最宽松;部分模型有商用限制 |
| 社区活跃度 | 影响 bug 修复速度和第三方工具适配 |
| 量化版本丰富度 | GGUF 格式版本越多,部署选择越灵活 |
3.3 模型命名规则解读
开源模型名称往往包含关键信息:
- 8B / 72B:参数量,十亿为单位。参数越大,能力越强,资源需求也越高。
- Q4_K_M / Q5_K_M / Q8_0:量化级别。Q 后面的数字是精度位数,数字越大精度越高、占用越大。K_M 表示一种混合量化策略。
- Instruct / Chat:经过指令微调,适合对话和指令执行。Base 版是未微调的基座模型。
- GGUF / AWQ / GPTQ:模型文件格式,对应不同推理引擎。
前端迁移提示:选模型就像选 npm 包——先看功能是否匹配(参数量/能力),再看许可证(能否商用),最后看社区活跃度(有没有人维护)。别选那个 star 最多但三个月没更新的。
4. 硬件需求指南
4.1 显存与参数量对应关系
模型运行时需要把权重加载到显存(GPU)或内存(CPU)中。粗略估算:
| 模型规模 | FP16 显存需求 | Q4 量化显存需求 | 推荐硬件 |
|---|---|---|---|
| 1-3B | 4-6 GB | 2-3 GB | 任意现代设备 |
| 7-8B | 16 GB | 6-8 GB | 8GB+ 显卡 / M 系列 Mac |
| 14B | 28 GB | 10-14 GB | 16GB+ 显卡 / 16GB Mac |
| 32-34B | 64 GB | 20-24 GB | 24GB 显卡 / 32GB Mac |
| 70-72B | 140 GB | 40-48 GB | 多卡 / 64GB+ Mac |
4.2 Apple Silicon 方案
M 系列芯片的统一内存架构是跑本地模型的天然优势——CPU 和 GPU 共享同一块内存,不存在显存瓶颈。
- M2/M3/M4(16GB):舒适运行 7-8B Q4 模型,可以勉强跑 14B Q4。
- M2/M3/M4 Pro(18-36GB):14B 无压力,32B Q4 可以尝试。
- M2/M3/M4 Max(36-128GB):70B Q4 可行,是消费级设备的天花板。
- M2/M4 Ultra(128-192GB):可以跑完整精度的 70B,甚至更大模型。
4.3 消费级 GPU
- RTX 3090 / 4090(24GB):能跑 14B FP16 或 32B Q4,是消费级的性价比之选。
- RTX 4060/4070(8-12GB):只够跑 7-8B Q4,但足以完成开发和原型验证。
- 双卡方案:部分引擎支持多卡并行,两张 3090 可以合并为 48GB 显存。
4.4 云 GPU 租用
不想买硬件的团队可以按需租用:
- AutoDL:国内平台,A100/H100 按小时计费,适合国内用户。
- RunPod:海外平台,支持 serverless GPU,按秒计费。
- Lambda Labs:面向 AI 训练/推理的云 GPU,有长期租赁折扣。
前端迁移提示:显存之于 LLM,就像内存之于浏览器标签页——模型加载到显存就像标签页占内存,模型越大占得越多,超出容量就崩溃(OOM)。量化就是把每个标签页的内存占用压下来。
5. 模型量化入门
5.1 什么是量化
模型原始训练通常使用 FP32 或 FP16 精度存储权重。量化是指用更低精度(INT8、INT4 甚至更低)来近似表示这些权重。精度降低意味着文件更小、推理更快、显存占用更低,代价是生成质量有一定损失。
精度从高到低:FP16 → INT8 → INT4。实践中 INT4 量化的质量损失通常可以接受,尤其是 7B 以上的模型。
5.2 常见量化格式
- GGUF:llama.cpp 生态的标准格式,Ollama 直接支持。社区提供大量预量化版本,从 HuggingFace 下载即用。
- AWQ:Activation-aware Weight Quantization,针对 GPU 推理优化,vLLM 原生支持。
- GPTQ:经典 GPU 量化方案,精度和速度的平衡点好,vLLM 和 HuggingFace Transformers 支持。
5.3 量化级别对比
| 量化级别 | 精度 | 文件大小(7B) | 质量损失 | 推荐场景 |
|---|---|---|---|---|
| Q8_0 | 8-bit | ~7 GB | 极小 | 显存充裕时首选 |
| Q5_K_M | 5-bit 混合 | ~5 GB | 很小 | 质量与大小的最佳平衡 |
| Q4_K_M | 4-bit 混合 | ~4 GB | 小 | 显存有限时的推荐选择 |
| Q3_K_M | 3-bit 混合 | ~3 GB | 可感知 | 极端显存限制 |
| Q2_K | 2-bit | ~2.5 GB | 明显 | 仅用于测试 |
5.4 实操:获取 GGUF 模型给 Ollama
Ollama 内置了常用模型的拉取能力,最简单的方式:
bash
# 拉取 Qwen3 8B(Ollama 自动选择合适的量化版本)
ollama pull qwen3:8b
# 指定特定量化版本
ollama pull qwen3:8b-q4_K_M
# 查看已下载的模型
ollama list如果需要使用 HuggingFace 上的自定义 GGUF 模型,可以创建 Modelfile:
text
FROM ./your-model.gguf
PARAMETER temperature 0.7
PARAMETER num_ctx 8192
SYSTEM "你是一个有帮助的助手。"bash
ollama create my-model -f Modelfile前端迁移提示:量化就像图片压缩——JPEG quality 100 是无损的但文件巨大,quality 80 肉眼几乎看不出差别但体积减半,quality 30 就明显模糊了。Q4_K_M 大致相当于 quality 80 的位置。
6. Embedding 模型自托管
6.1 为什么 Embedding 也要本地化
RAG 系统中,Embedding 模型负责把文本转成向量。每次检索都需要调用 Embedding API。如果使用云端服务:
- 所有待索引的文档内容都要发送到第三方。
- 每次用户查询都产生 API 调用费用。
- 离线环境下 RAG 直接瘫痪。
本地 Embedding 模型解决了这三个问题,且性能通常不逊于云端。
6.2 推荐选型思路
- BGE-M3:多语言、多粒度检索,支持稠密、稀疏和多向量三种检索模式。中文检索场景的开源首选。
- Nomic-Embed-Text:Ollama 原生支持,安装拉取一条命令搞定。轻量高效,适合快速上手。
- jina-embeddings-v3:多语言支持好,长文本处理能力强,适合需要处理长文档的场景。
6.3 Ollama Embedding API 用法
bash
# 拉取 Embedding 模型
ollama pull bge-m3
# 或轻量选择
ollama pull nomic-embed-texttypescript
// 调用 Ollama Embedding API
async function getEmbedding(text: string): Promise<number[]> {
const response = await fetch('http://localhost:11434/api/embeddings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'bge-m3',
prompt: text,
}),
});
const data = await response.json();
return data.embedding;
}
// 批量生成 Embedding
async function getEmbeddings(texts: string[]): Promise<number[][]> {
return Promise.all(texts.map(getEmbedding));
}6.4 与云端 Embedding 的性能参考
| 维度 | text-embedding-3-small | BGE-M3(本地) | nomic-embed-text(本地) |
|---|---|---|---|
| 中文检索质量 | 良好 | 优秀 | 良好 |
| 英文检索质量 | 优秀 | 优秀 | 优秀 |
| 维度 | 1536 | 1024 | 768 |
| 速度(单条) | 取决于网络 | ~50ms(GPU) | ~20ms(GPU) |
| 成本 | 按 token 计费 | 免费 | 免费 |
| 离线可用 | 否 | 是 | 是 |
7. 向量数据库自建
7.1 主流选型
- ChromaDB:Python/JS 双客户端,Docker 一键启动,API 简洁。适合开发阶段和中小规模数据。
- Qdrant:Rust 编写,高性能,支持稠密+稀疏混合检索和丰富的过滤条件。适合生产环境。
- Milvus Lite:Milvus 的单机版,适合中等规模。完整版 Milvus 支持大规模分布式。
- pgvector:PostgreSQL 扩展,如果团队已经在用 PG,不需要额外引入新组件。
7.2 对比表格
| 维度 | ChromaDB | Qdrant | Milvus Lite | pgvector |
|---|---|---|---|---|
| 安装难度 | 极低 | 低(Docker) | 中等 | 低(PG 扩展) |
| 性能 | 中等 | 高 | 高 | 中等 |
| 水平扩展 | 有限 | 支持 | 支持 | 依赖 PG |
| 混合检索 | 有限 | 原生支持 | 支持 | 需额外配置 |
| TypeScript SDK | 官方支持 | 官方支持 | 社区维护 | 通过 PG 驱动 |
| 适合阶段 | 开发/小规模 | 生产 | 中大规模生产 | 已有 PG 的团队 |
7.3 ChromaDB + 本地 Embedding 完整 RAG 链路
typescript
import { ChromaClient } from 'chromadb';
// 初始化 ChromaDB 客户端(假设 Docker 运行在默认端口)
const chroma = new ChromaClient({ path: 'http://localhost:8000' });
// Embedding 函数:调用本地 Ollama
async function ollamaEmbed(texts: string[]): Promise<number[][]> {
const embeddings: number[][] = [];
for (const text of texts) {
const res = await fetch('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();
embeddings.push(data.embedding);
}
return embeddings;
}
// 创建集合并导入文档
async function indexDocuments(docs: { id: string; text: string }[]) {
const collection = await chroma.getOrCreateCollection({ name: 'knowledge' });
const texts = docs.map(d => d.text);
const embeddings = await ollamaEmbed(texts);
await collection.add({
ids: docs.map(d => d.id),
documents: texts,
embeddings,
});
}
// 检索相关文档
async function retrieve(query: string, topK = 3) {
const collection = await chroma.getCollection({ name: 'knowledge' });
const queryEmbedding = await ollamaEmbed([query]);
const results = await collection.query({
queryEmbeddings: queryEmbedding,
nResults: topK,
});
return results.documents?.[0] ?? [];
}
// 完整 RAG:检索 + 生成
async function ragQuery(question: string): Promise<string> {
const docs = await retrieve(question);
const context = docs.join('\n---\n');
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen3:8b',
stream: false,
messages: [
{
role: 'system',
content: `基于以下参考资料回答问题。如果资料不足以回答,明确说明。\n\n${context}`,
},
{ role: 'user', content: question },
],
}),
});
const data = await response.json();
return data.message.content;
}8. API Gateway 与 OpenAI 兼容层
自托管方案的一个关键优势是:现有基于 OpenAI SDK 的代码几乎不用改。
8.1 Ollama 的 OpenAI 兼容 API
Ollama 默认在 http://localhost:11434 提供 OpenAI 兼容的端点:
POST /v1/chat/completions → 对话生成
POST /v1/embeddings → 文本嵌入
GET /v1/models → 模型列表这意味着你只需要改两个参数就能让现有代码跑在本地模型上:
typescript
import OpenAI from 'openai';
// 云端版本
const cloudClient = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// 本地版本——只改 baseURL 和 apiKey
const localClient = new OpenAI({
baseURL: 'http://localhost:11434/v1',
apiKey: 'ollama', // Ollama 不校验 key,但 SDK 要求传
});
// 相同的调用代码,无需任何修改
async function chat(client: OpenAI, question: string) {
const response = await client.chat.completions.create({
model: 'qwen3:8b', // 本地模型名
messages: [{ role: 'user', content: question }],
});
return response.choices[0].message.content;
}8.2 LiteLLM 统一代理
当你的系统需要同时调用多个模型(本地 + 云端),LiteLLM 提供了统一的代理层:
yaml
# litellm config.yaml
model_list:
- model_name: local-qwen
litellm_params:
model: ollama/qwen3:8b
api_base: http://localhost:11434
- model_name: cloud-gpt4
litellm_params:
model: gpt-4o
api_key: sk-xxx启动后所有模型通过同一个端点访问,切换模型只需要改 model 参数。
前端迁移提示:就像 axios 的
baseURL配置——开发环境指向localhost:11434,生产环境指向api.openai.com,业务代码完全不改。用环境变量切换就行。
9. 前端集成方案
9.1 方案对比
| 方案 | 改动量 | 依赖 | 流式支持 | 适合场景 |
|---|---|---|---|---|
| openai SDK + baseURL 替换 | 最小 | openai | 是 | 已有 OpenAI 代码的项目 |
| Vercel AI SDK + 自定义 provider | 中等 | ai, @ai-sdk/* | 是 | 新项目/全栈 Next.js |
| 原生 fetch + Ollama API | 零依赖 | 无 | 是 | 不想引入额外包 |
| node-llama-cpp | 中等 | node-llama-cpp | 是 | 不想起独立服务 |
| Wllama(WebAssembly) | 较大 | wllama | 是 | 纯浏览器推理、零后端 |
9.2 推荐方案:Next.js + Ollama + 流式输出
typescript
// app/api/chat/route.ts — Next.js Route Handler
import { streamText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
const ollama = createOpenAI({
baseURL: 'http://localhost:11434/v1',
apiKey: 'ollama',
});
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: ollama('qwen3:8b'),
system: '你是一个有帮助的助手。基于用户问题给出简洁准确的回答。',
messages,
});
return result.toDataStreamResponse();
}typescript
// app/page.tsx — 前端流式 UI
'use client';
import { useChat } from '@ai-sdk/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
return (
<div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
<div>
{messages.map(m => (
<div key={m.id} style={{ margin: '12px 0' }}>
<strong>{m.role === 'user' ? '你' : 'AI'}:</strong>
<span>{m.content}</span>
</div>
))}
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: 8 }}>
<input
value={input}
onChange={handleInputChange}
placeholder="输入消息..."
style={{ flex: 1, padding: 8 }}
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>发送</button>
</form>
</div>
);
}这套代码与使用云端 API 的版本唯一的区别就是 createOpenAI 的 baseURL 参数。想切回云端,改一行配置即可。
9.3 浏览器内推理:Wllama
Wllama 将 llama.cpp 编译为 WebAssembly,可以在浏览器中直接运行小模型。完全不需要后端服务,模型文件从 CDN 或本地加载。
适合场景:离线 Web 应用、对隐私要求极高的客户端、边缘设备上的 PWA。限制是只能跑小模型(1-3B),且首次加载模型文件较大。
10. 安全与运维
10.1 内容安全
本地模型没有云端 API 提供商的内容过滤层。这意味着:
- 模型可能生成有害或不准确的内容。
- 你需要自建 Guardrails:输入过滤(敏感词、注入检测)+ 输出过滤(合规检查、格式校验)。
- 开源方案如 Guardrails AI、NeMo Guardrails 可以作为起点。
10.2 模型更新策略
- 用版本号管理模型文件,不要直接覆盖。
- 新版本先在测试环境验证,确认质量不退化。
- 保留回滚能力:至少保留前一个版本的模型文件。
- 建议用环境变量或配置中心控制当前使用的模型版本。
10.3 监控指标
| 指标 | 健康标准 | 告警阈值 |
|---|---|---|
| 推理延迟(P95) | < 2s(7B)/ < 5s(70B) | 超过 2x 基准值 |
| GPU 利用率 | 60-90% | 持续 > 95% |
| 显存使用 | < 90% | > 95% |
| 请求队列长度 | < 10 | > 50 |
| 生成失败率 | < 1% | > 5% |
10.4 Docker Compose 一键部署
yaml
# docker-compose.yml
version: '3.8'
services:
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
chromadb:
image: chromadb/chroma:latest
ports:
- "8000:8000"
volumes:
- chroma_data:/chroma/chroma
app:
build: .
ports:
- "3000:3000"
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- CHROMA_URL=http://chromadb:8000
depends_on:
- ollama
- chromadb
volumes:
ollama_data:
chroma_data:bash
# 启动所有服务
docker compose up -d
# 进入 ollama 容器拉取模型
docker compose exec ollama ollama pull qwen3:8b
docker compose exec ollama ollama pull bge-m311. 实战路径:从零搭建本地 AI Agent
Step 1:安装 Ollama 并拉取模型
bash
# macOS / Linux
curl -fsSL https://ollama.com/install.sh | sh
# 拉取推理模型和 Embedding 模型
ollama pull qwen3:8b
ollama pull bge-m3Step 2:验证模型响应
bash
# 命令行快速测试
curl http://localhost:11434/api/chat -d '{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "用一句话解释什么是 RAG"}],
"stream": false
}'typescript
// TypeScript 验证
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen3:8b',
stream: false,
messages: [{ role: 'user', content: '用一句话解释什么是 RAG' }],
}),
});
const data = await response.json();
console.log(data.message.content);Step 3:安装 ChromaDB
bash
# Docker 方式
docker run -d -p 8000:8000 chromadb/chroma
# 验证
curl http://localhost:8000/api/v1/heartbeatStep 4:生成 Embedding 并导入向量库
typescript
import { ChromaClient } from 'chromadb';
const chroma = new ChromaClient({ path: 'http://localhost:8000' });
const docs = [
{ id: '1', text: 'RAG 是检索增强生成的缩写,通过检索外部知识来增强 LLM 的回答质量。' },
{ id: '2', text: 'Tool Calling 允许 LLM 调用外部工具来完成它无法直接完成的任务。' },
{ id: '3', text: 'Agent 是具有自主决策能力的 AI 系统,能够规划、执行和反思。' },
];
async function indexAll() {
const collection = await chroma.getOrCreateCollection({ name: 'knowledge' });
for (const doc of docs) {
const res = await fetch('http://localhost:11434/api/embeddings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'bge-m3', prompt: doc.text }),
});
const { embedding } = await res.json();
await collection.add({
ids: [doc.id],
documents: [doc.text],
embeddings: [embedding],
});
}
console.log(`已导入 ${docs.length} 条文档`);
}
indexAll();Step 5:实现 RAG 检索 + 生成
typescript
async function ragAnswer(question: string) {
// 1. 生成问题的 Embedding
const qRes = await fetch('http://localhost:11434/api/embeddings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'bge-m3', prompt: question }),
});
const { embedding } = await qRes.json();
// 2. 检索相关文档
const collection = await chroma.getCollection({ name: 'knowledge' });
const results = await collection.query({
queryEmbeddings: [embedding],
nResults: 2,
});
const context = results.documents?.[0]?.join('\n') ?? '';
// 3. 生成回答
const chatRes = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen3:8b',
stream: false,
messages: [
{
role: 'system',
content: `基于以下参考资料回答问题,不要编造信息:\n\n${context}`,
},
{ role: 'user', content: question },
],
}),
});
const chatData = await chatRes.json();
return chatData.message.content;
}
// 测试
const answer = await ragAnswer('什么是 Tool Calling?');
console.log(answer);Step 6:添加 Tool Calling
typescript
async function agentWithTools(question: string) {
const tools = [
{
type: 'function' as const,
function: {
name: 'search_knowledge',
description: '搜索知识库获取相关信息',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索关键词' },
},
required: ['query'],
},
},
},
{
type: 'function' as const,
function: {
name: 'get_current_time',
description: '获取当前时间',
parameters: { type: 'object', properties: {} },
},
},
];
// 通过 OpenAI 兼容接口调用 Tool Calling
const response = await fetch('http://localhost:11434/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen3:8b',
messages: [{ role: 'user', content: question }],
tools,
}),
});
const data = await response.json();
const message = data.choices[0].message;
// 如果模型决定调用工具
if (message.tool_calls) {
for (const call of message.tool_calls) {
const { name, arguments: args } = call.function;
let result: string;
if (name === 'search_knowledge') {
const parsed = JSON.parse(args);
result = await ragAnswer(parsed.query);
} else if (name === 'get_current_time') {
result = new Date().toISOString();
} else {
result = '未知工具';
}
console.log(`工具调用: ${name} → ${result}`);
}
} else {
console.log(`直接回答: ${message.content}`);
}
}Step 7:前端集成
使用第 9 节中的 Next.js + Vercel AI SDK 方案,将上述后端逻辑包装为 API Route,前端通过 useChat Hook 实现流式交互。完整代码见第 9.2 节。
12. 常见失败模式
12.1 模型下载慢或失败
原因:默认从海外源下载,大模型文件动辄几 GB。
解决:配置 Ollama 镜像源,或手动从 HuggingFace 镜像站(如 hf-mirror.com)下载 GGUF 文件后本地导入。
12.2 显存不足 OOM
原因:模型参数量超出可用显存。
解决:换更低的量化版本(Q4→Q3→Q2),或换更小的模型(14B→8B→3B)。OOM 不是模型的问题,是选型不匹配硬件。
12.3 Tool Calling 格式错误
原因:不是所有开源模型都支持 Tool Calling,即使支持的模型在格式解析上也可能不稳定。
解决:优先选择 Qwen、Mistral 等 Tool Calling 成熟的模型;在 Prompt 中给出清晰的工具调用示例;增加输出格式校验和重试逻辑。
12.4 本地推理太慢
原因:模型在 CPU 上推理(未利用 GPU),或模型过大。
解决:确认 GPU 驱动和 CUDA 已正确安装;在 Mac 上确认 Ollama 使用了 Metal 加速;降低模型参数量或量化级别。首 token 延迟和生成速度是两个不同指标,前者受模型大小影响更大。
12.5 向量检索不准
原因:Embedding 模型与实际数据领域不匹配,或文档分块策略不合理。
解决:换更强的 Embedding 模型(nomic→BGE-M3);优化分块策略(按语义而非固定长度);增加重排序步骤(Reranker)。
13. 验证方式
完成本专题后,你应该能做到:
- 在自己的设备上跑通从文档导入到 RAG 问答再到 Tool Calling 的完整 Agent 链路。
- 清楚说明 Ollama、vLLM、llama.cpp 各自的定位——Ollama 像 Docker(简单易用),vLLM 像 Nginx(高并发生产),llama.cpp 像 C 标准库(底层高效)。
- 根据硬件条件(有什么 GPU、多少内存)选出合适的模型和量化方案,而不是盲目选最大的。
- 将一段基于
openaiSDK 的云端代码,在不改业务逻辑的前提下切换为本地模型运行。
14. 面试表达模板
以下是几种场景下的表达参考:
通用自建经验
"我搭建过完全自托管的 AI Agent 系统,推理层使用 Ollama 加载 Qwen 系列模型,Embedding 用的是 BGE-M3,向量库选了 ChromaDB。整套系统通过 Docker Compose 管理,数据完全不出域,满足了内网部署的合规要求。"
技术选型
"在数据合规场景下,我对比了 Ollama、vLLM 和 llama.cpp 三种推理引擎。考虑到团队规模和并发需求,开发阶段选了 Ollama 快速迭代,生产环境切到了 vLLM 来应对多用户并发。模型选了 Qwen 系列,因为中文 Tool Calling 支持最成熟。"
迁移经验
"项目早期用的 OpenAI API,后来因为成本和合规需求迁移到自托管。我设计了 baseURL 可配置的架构,业务代码不需要修改,只通过环境变量切换云端和本地模型。迁移过程中发现本地模型在 Tool Calling 的格式稳定性上不如云端,于是加了输出校验和重试机制来弥补。"
诚实说明差距
"本地模型和云端顶级模型之间确实有能力差距,尤其是复杂推理和长文本理解方面。但在大多数业务场景下,8B 量化模型已经够用了。不够的地方我通过 RAG 增强上下文、通过 Prompt 工程约束输出格式来补偿。对于少数确实需要顶级能力的场景,系统会 fallback 到云端 API。"
15. 验收清单
- [ ] 能独立完成 Ollama 安装和模型拉取,并通过 API 验证模型响应。
- [ ] 能解释 Ollama、vLLM、llama.cpp 的定位差异,根据场景选择合适的引擎。
- [ ] 能根据硬件条件(显存/内存)选择合适的模型参数量和量化方案。
- [ ] 能搭建本地 Embedding 模型 + 向量数据库,完成文档导入和检索。
- [ ] 能将现有基于 OpenAI SDK 的 Agent 代码迁移到本地模型,只改配置不改逻辑。
- [ ] 能用 Docker Compose 部署包含 Ollama + ChromaDB + 应用层的完整自托管 Agent。
- [ ] 能识别并处理本地模型的常见问题:OOM、Tool Calling 格式、推理性能。