Skip to content

专题 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 对比表格

维度OllamavLLMllama.cppLocalAI
安装难度极低(一键安装)中等(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 CallingQwen、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-3B4-6 GB2-3 GB任意现代设备
7-8B16 GB6-8 GB8GB+ 显卡 / M 系列 Mac
14B28 GB10-14 GB16GB+ 显卡 / 16GB Mac
32-34B64 GB20-24 GB24GB 显卡 / 32GB Mac
70-72B140 GB40-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_08-bit~7 GB极小显存充裕时首选
Q5_K_M5-bit 混合~5 GB很小质量与大小的最佳平衡
Q4_K_M4-bit 混合~4 GB显存有限时的推荐选择
Q3_K_M3-bit 混合~3 GB可感知极端显存限制
Q2_K2-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-text
typescript
// 调用 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-smallBGE-M3(本地)nomic-embed-text(本地)
中文检索质量良好优秀良好
英文检索质量优秀优秀优秀
维度15361024768
速度(单条)取决于网络~50ms(GPU)~20ms(GPU)
成本按 token 计费免费免费
离线可用

7. 向量数据库自建

7.1 主流选型

  • ChromaDB:Python/JS 双客户端,Docker 一键启动,API 简洁。适合开发阶段和中小规模数据。
  • Qdrant:Rust 编写,高性能,支持稠密+稀疏混合检索和丰富的过滤条件。适合生产环境。
  • Milvus Lite:Milvus 的单机版,适合中等规模。完整版 Milvus 支持大规模分布式。
  • pgvector:PostgreSQL 扩展,如果团队已经在用 PG,不需要额外引入新组件。

7.2 对比表格

维度ChromaDBQdrantMilvus Litepgvector
安装难度极低低(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 的版本唯一的区别就是 createOpenAIbaseURL 参数。想切回云端,改一行配置即可。

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-m3

11. 实战路径:从零搭建本地 AI Agent

Step 1:安装 Ollama 并拉取模型

bash
# macOS / Linux
curl -fsSL https://ollama.com/install.sh | sh

# 拉取推理模型和 Embedding 模型
ollama pull qwen3:8b
ollama pull bge-m3

Step 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/heartbeat

Step 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、多少内存)选出合适的模型和量化方案,而不是盲目选最大的。
  • 将一段基于 openai SDK 的云端代码,在不改业务逻辑的前提下切换为本地模型运行。

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 格式、推理性能。