附录F:本地模拟生成式引擎环境(Docker Compose + Ollama)
F.1 概述与目的
在生成式引擎优化(GEO)的实战中,能够在本地模拟目标生成式搜索引擎(如 Perplexity、Bing Chat、豆包、DeepSeek)的引用与回答逻辑,是工程师进行策略验证、内容调试和效果预判的核心能力。
本附录提供一套基于 Docker Compose 和 Ollama 的本地模拟环境搭建方案。该环境可以:
- 在完全离线或内网环境中运行。
- 加载主流开源大模型(如 LLaMA、Mistral、Qwen)。
- 通过 RAG(检索增强生成)模拟生成式引擎的“检索+回答”流程。
- 用于测试你的内容被模型引用和生成答案的可能性。
F.2 环境架构
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Docker Compose | | Ollama Server | | RAG 应用层 |
| (编排层) | ----> | (模型推理层) | ----> | (模拟引擎) |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
| | |
| 定义服务、网络、卷 | 管理模型下载与推理 | 实现检索、Prompt、输出
| | |
v v v
+-------------------+ +-------------------+ +-------------------+
| ChromaDB | | Nginx 反向代理 | | Web UI (可选) |
| (向量数据库) | | (路由与日志) | | (Open WebUI) |
+-------------------+ +-------------------+ +-------------------+
F.3 环境准备
3.1 系统要求
- 操作系统:Linux / macOS / Windows(WSL2)
- Docker:20.10+
- Docker Compose:v2.0+
- 内存:建议 ≥ 16GB(运行 7B 模型至少需要 8GB)
- 磁盘:模型文件约 4-8GB(7B 模型)
3.2 安装 Docker 与 Docker Compose
# Ubuntu/Debian
sudo apt update
sudo apt install docker.io docker-compose-v2
# macOS (使用 Homebrew)
brew install docker docker-compose
# Windows (使用 WSL2 + Docker Desktop)
# 参考官方文档:https://docs.docker.com/desktop/wsl/
3.3 验证安装
docker --version
docker compose version
F.4 快速部署方案
4.1 项目目录结构
geo-local-engine/
├── docker-compose.yml
├── .env
├── ollama/
│ └── Dockerfile (可选,用于自定义)
├── chromadb/
│ └── data/ # 向量数据库持久化
├── rag-app/
│ ├── Dockerfile
│ ├── app.py # RAG 核心逻辑
│ └── requirements.txt
├── nginx/
│ ├── nginx.conf
│ └── logs/
└── data/
└── your_content/ # 放入你的待测试内容
4.2 docker-compose.yml 文件
version: '3.8'
services:
# Ollama 服务 - 模型推理引擎
ollama:
image: ollama/ollama:latest
container_name: geo-ollama
ports:
- "11434:11434"
volumes:
- ./ollama/data:/root/.ollama
environment:
- OLLAMA_KEEP_ALIVE=24h
- OLLAMA_NUM_PARALLEL=4
- OLLAMA_MAX_LOADED_MODELS=2
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu] # 如有GPU,取消注释
restart: unless-stopped
networks:
- geo-net
# ChromaDB - 向量数据库(存储内容嵌入)
chromadb:
image: chromadb/chroma:latest
container_name: geo-chromadb
ports:
- "8000:8000"
volumes:
- ./chromadb/data:/chroma/chroma
environment:
- IS_PERSISTENT=TRUE
- PERSIST_DIRECTORY=/chroma/chroma
- ANONYMIZED_TELEMETRY=FALSE
restart: unless-stopped
networks:
- geo-net
# RAG 应用 - 模拟生成引擎
rag-app:
build: ./rag-app
container_name: geo-rag-app
ports:
- "8080:8080"
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- CHROMA_HOST=chromadb
- CHROMA_PORT=8000
- MODEL_NAME=qwen2.5:7b # 可改为其他模型
- EMBEDDING_MODEL=bge-m3:latest
- COLLECTION_NAME=geo_test
volumes:
- ./data:/app/data
depends_on:
- ollama
- chromadb
restart: unless-stopped
networks:
- geo-net
# Nginx 反向代理(可选,用于日志和路由)
nginx:
image: nginx:alpine
container_name: geo-nginx
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/logs:/var/log/nginx
depends_on:
- rag-app
restart: unless-stopped
networks:
- geo-net
# Open WebUI(可选,提供图形界面)
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: geo-webui
ports:
- "3000:8080"
volumes:
- ./open-webui/data:/app/backend/data
environment:
- OLLAMA_BASE_URL=http://ollama:11434
depends_on:
- ollama
restart: unless-stopped
networks:
- geo-net
networks:
geo-net:
driver: bridge
4.3 .env 配置文件
# 模型选择
# 中文推荐:qwen2.5:7b, qwen2.5:14b, yi:34b
# 英文推荐:llama3.1:8b, mistral:7b, mixtral:8x7b
MODEL_NAME=qwen2.5:7b
EMBEDDING_MODEL=bge-m3:latest
# Ollama 配置
OLLAMA_KEEP_ALIVE=24h
OLLAMA_NUM_PARALLEL=4
# ChromaDB 配置
CHROMA_PERSIST_DIR=/chroma/chroma
F.5 RAG 应用核心代码
5.1 app.py(简化版)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import ollama
import chromadb
from chromadb.utils import embedding_functions
import os
import logging
app = FastAPI(title="本地生成引擎模拟器")
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 环境变量
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://ollama:11434")
CHROMA_HOST = os.getenv("CHROMA_HOST", "chromadb")
CHROMA_PORT = int(os.getenv("CHROMA_PORT", "8000"))
MODEL_NAME = os.getenv("MODEL_NAME", "qwen2.5:7b")
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "bge-m3:latest")
COLLECTION_NAME = os.getenv("COLLECTION_NAME", "geo_test")
# 初始化 Ollama 客户端
ollama_client = ollama.Client(host=OLLAMA_BASE_URL)
# 初始化 ChromaDB 客户端
chroma_client = chromadb.HttpClient(host=CHROMA_HOST, port=CHROMA_PORT)
embedding_func = embedding_functions.OllamaEmbeddingFunction(
model_name=EMBEDDING_MODEL,
url=OLLAMA_BASE_URL
)
# 获取或创建集合
try:
collection = chroma_client.get_collection(
name=COLLECTION_NAME,
embedding_function=embedding_func
)
logger.info(f"已加载现有集合: {COLLECTION_NAME}")
except:
collection = chroma_client.create_collection(
name=COLLECTION_NAME,
embedding_function=embedding_func
)
logger.info(f"已创建新集合: {COLLECTION_NAME}")
class QueryRequest(BaseModel):
query: str
top_k: int = 5
use_rag: bool = True
system_prompt: str = "你是一个专业的搜索助手。请基于提供的上下文信息,用中文回答用户问题。如果信息不足,请明确说明。"
class IngestRequest(BaseModel):
file_path: str = "/app/data"
chunk_size: int = 500
chunk_overlap: int = 50
@app.get("/health")
async def health():
"""健康检查"""
return {"status": "ok", "model": MODEL_NAME, "collection": COLLECTION_NAME}
@app.post("/ingest")
async def ingest_content(request: IngestRequest):
"""导入内容到向量数据库"""
import glob
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=request.chunk_size,
chunk_overlap=request.chunk_overlap
)
files = glob.glob(f"{request.file_path}/**/*.md", recursive=True) + \
glob.glob(f"{request.file_path}/**/*.txt", recursive=True) + \
glob.glob(f"{request.file_path}/**/*.html", recursive=True)
if not files:
raise HTTPException(status_code=404, detail="未找到可导入的文件")
imported_count = 0
for file_path in files:
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
chunks = text_splitter.split_text(content)
for i, chunk in enumerate(chunks):
collection.add(
documents=[chunk],
metadatas=[{
"source": file_path,
"chunk_index": i,
"total_chunks": len(chunks)
}],
ids=[f"{file_path}_{i}"]
)
imported_count += 1
logger.info(f"已导入: {file_path} ({len(chunks)} 块)")
except Exception as e:
logger.error(f"导入失败: {file_path} - {str(e)}")
return {"imported_chunks": imported_count, "files_processed": len(files)}
@app.post("/query")
async def query(request: QueryRequest):
"""模拟生成引擎的问答流程"""
# 步骤1: 检索相关上下文
if request.use_rag:
results = collection.query(
query_texts=[request.query],
n_results=request.top_k
)
if results['documents'] and results['documents'][0]:
context = "\n\n".join([
f"[来源: {meta['source']}]\n{doc}"
for doc, meta in zip(results['documents'][0], results['metadatas'][0])
])
sources = [
{
"content": doc[:200] + "...",
"source": meta['source'],
"relevance": score
}
for doc, meta, score in zip(
results['documents'][0],
results['metadatas'][0],
results['distances'][0]
)
]
else:
context = "未找到相关上下文。"
sources = []
else:
context = ""
sources = []
# 步骤2: 构建 Prompt
messages = [
{"role": "system", "content": request.system_prompt},
{"role": "user", "content": f"上下文信息:\n{context}\n\n用户问题:{request.query}"}
]
# 步骤3: 调用 Ollama 生成回答
try:
response = ollama_client.chat(
model=MODEL_NAME,
messages=messages,
options={
"temperature": 0.3,
"top_p": 0.9,
"max_tokens": 2048
}
)
return {
"query": request.query,
"answer": response['message']['content'],
"sources": sources,
"model": MODEL_NAME,
"rag_enabled": request.use_rag
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"模型推理失败: {str(e)}")
@app.post("/compare")
async def compare_answers(request: QueryRequest):
"""对比有无 RAG 的回答差异"""
# 有 RAG
rag_response = await query(QueryRequest(
query=request.query,
use_rag=True,
system_prompt=request.system_prompt
))
# 无 RAG(纯模型知识)
no_rag_response = await query(QueryRequest(
query=request.query,
use_rag=False,
system_prompt=request.system_prompt
))
return {
"query": request.query,
"with_rag": rag_response,
"without_rag": no_rag_response
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)
5.2 requirements.txt
fastapi==0.104.1
uvicorn[standard]==0.24.0
ollama==0.1.9
chromadb==0.4.22
langchain==0.1.0
pydantic==2.5.2
python-multipart==0.0.6
5.3 Dockerfile(rag-app)
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY app.py .
# 创建数据目录
RUN mkdir -p /app/data
# 暴露端口
EXPOSE 8080
# 启动命令
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
5.4 nginx.conf(可选)
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;
upstream rag_app {
server rag-app:8080;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://rag_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/ {
proxy_pass http://rag_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
F.6 启动与使用
6.1 启动环境
# 进入项目目录
cd geo-local-engine
# 首次启动(会自动拉取镜像)
docker compose up -d
# 查看日志
docker compose logs -f
# 检查服务状态
docker compose ps
6.2 下载模型
# 进入 Ollama 容器
docker exec -it geo-ollama /bin/bash
# 下载中文模型(推荐)
ollama pull qwen2.5:7b
ollama pull bge-m3:latest
# 下载英文模型(可选)
ollama pull llama3.1:8b
ollama pull nomic-embed-text:latest
# 退出容器
exit
6.3 导入待测试内容
# 将你的内容放入 data/ 目录
# 支持格式:.md, .txt, .html
# 调用导入 API
curl -X POST http://localhost:8080/ingest \
-H "Content-Type: application/json" \
-d '{
"file_path": "/app/data",
"chunk_size": 500,
"chunk_overlap": 50
}'
6.4 模拟查询
# 基础查询(带 RAG)
curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{
"query": "你的产品的主要功能是什么?",
"top_k": 5,
"use_rag": true
}'
# 对比查询(有/无 RAG)
curl -X POST http://localhost:8080/compare \
-H "Content-Type: application/json" \
-d '{
"query": "你的产品的主要功能是什么?"
}'
6.5 使用 Open WebUI(图形界面)
访问 http://localhost:3000,首次使用需注册账号。 在设置中选择模型为 qwen2.5:7b。
F.7 高级配置与优化
7.1 使用 GPU 加速
在 docker-compose.yml 中取消 GPU 相关注释:
services:
ollama:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
并确保已安装 NVIDIA Container Toolkit:
# Ubuntu
sudo apt install nvidia-container-toolkit
sudo systemctl restart docker
7.2 多模型配置
修改 .env 文件中的 MODEL_NAME:
# 使用不同模型
MODEL_NAME=yi:34b # 中文大模型(34B)
MODEL_NAME=mixtral:8x7b # 混合专家模型
MODEL_NAME=deepseek-coder:6.7b # 代码专用
7.3 自定义系统提示词
通过 API 参数自定义生成风格:
curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{
"query": "你的产品的主要功能是什么?",
"system_prompt": "你是一个严谨的技术文档助手。请用结构化方式回答,包含:1)功能概述 2)核心特性 3)使用场景。只基于提供的上下文回答。"
}'
7.4 批量测试脚本
# batch_test.py
import requests
import json
import time
BASE_URL = "http://localhost:8080"
test_queries = [
"你的产品的主要功能是什么?",
"你的产品与竞争对手相比有什么优势?",
"你的产品如何安装?",
"你的产品的价格是多少?",
"你的产品支持哪些平台?"
]
results = []
for query in test_queries:
response = requests.post(
f"{BASE_URL}/query",
json={"query": query, "use_rag": True}
)
result = response.json()
results.append({
"query": query,
"answer": result["answer"][:200],
"sources_count": len(result["sources"])
})
print(f"Query: {query}")
print(f"Answer: {result['answer'][:100]}...")
print(f"Sources: {len(result['sources'])}")
print("-" * 50)
time.sleep(1)
# 保存结果
with open("geo_test_results.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
F.8 监控与调试
8.1 检查服务日志
# 查看所有服务日志
docker compose logs -f
# 查看特定服务日志
docker compose logs rag-app -f
docker compose logs ollama -f
8.2 查看向量数据库内容
# 进入 ChromaDB 容器
docker exec -it geo-chromadb /bin/bash
# 使用 Python 检查
python3 -c "
import chromadb
client = chromadb.HttpClient(host='localhost', port=8000)
collection = client.get_collection('geo_test')
print(f'文档数量: {collection.count()}')
results = collection.get()
print(f'前3个文档: {results[\"documents\"][:3]}')
"
8.3 性能监控
# 查看资源使用
docker stats
# 查看 Ollama 模型加载状态
curl http://localhost:11434/api/tags
F.9 常见问题与排错
9.1 模型下载失败
# 检查网络连接
docker exec geo-ollama curl -I https://ollama.ai
# 使用国内镜像(如适用)
ollama pull qwen2.5:7b --insecure
9.2 内存不足
# 使用更小的模型
ollama pull qwen2.5:1.5b # 1.5B 参数,仅需 2GB 内存
# 限制并发
# 在 .env 中设置
OLLAMA_NUM_PARALLEL=1
OLLAMA_MAX_LOADED_MODELS=1
9.3 向量数据库连接失败
# 检查 ChromaDB 是否启动
docker compose ps chromadb
# 重启服务
docker compose restart chromadb
9.4 内容导入后查询无结果
# 检查内容是否成功导入
curl http://localhost:8080/health
# 检查集合中的文档数量
# 使用 Python 脚本验证
F.10 实际应用场景
10.1 测试内容在生成引擎中的表现
- 导入你的产品文档、博客文章、FAQ。
- 模拟用户提问,观察模型是否引用你的内容。
- 对比有/无 RAG 的回答差异。
10.2 优化结构化数据
- 测试不同 Schema 标记对模型理解的影响。
- 验证 JSON-LD 是否被正确解析。
10.3 竞品分析
- 导入竞争对手的公开内容。
- 对比你的内容在相同查询下的表现。
10.4 内容策略验证
- 测试不同写作风格(如问答式、列表式、段落式)的效果。
- 验证关键词密度和语义相关性的影响。
F.11 与生产环境的差异说明
| 特性 | 本地模拟环境 | 真实生成引擎 |
|---|---|---|
| 模型规模 | 7B-34B 参数 | 数百亿参数 |
| 训练数据 | 仅你导入的内容 | 全网数据 |
| 排名算法 | 简单的向量相似度 | 复杂的多因素排序 |
| 实时性 | 手动更新 | 自动爬取 |
| 个性化 | 无 | 基于用户历史 |
建议:将本地环境作为快速验证工具,最终优化策略仍需在真实生成引擎中验证。
F.12 一键部署脚本
创建一个 deploy.sh 脚本,简化部署流程:
#!/bin/bash
# 本地生成引擎环境一键部署脚本
echo "=== 开始部署本地生成引擎环境 ==="
# 1. 检查 Docker
if ! command -v docker &> /dev/null; then
echo "错误: 请先安装 Docker"
exit 1
fi
# 2. 创建目录结构
mkdir -p {ollama/data,chromadb/data,nginx/logs,data/your_content,rag-app}
# 3. 创建 docker-compose.yml
cat > docker-compose.yml << 'EOF'
# 在此粘贴上面的 docker-compose.yml 内容
EOF
# 4. 创建 .env 文件
cat > .env << 'EOF'
MODEL_NAME=qwen2.5:7b
EMBEDDING_MODEL=bge-m3:latest
OLLAMA_KEEP_ALIVE=24h
OLLAMA_NUM_PARALLEL=4
EOF
# 5. 启动服务
echo "启动 Docker 服务..."
docker compose up -d
# 6. 等待服务就绪
echo "等待服务就绪..."
sleep 10
# 7. 下载模型
echo "下载模型(这可能需要几分钟)..."
docker exec geo-ollama ollama pull qwen2.5:7b
docker exec geo-ollama ollama pull bge-m3:latest
# 8. 验证
echo "验证服务状态..."
docker compose ps
echo "=== 部署完成 ==="
echo "API 地址: http://localhost:8080"
echo "Web UI: http://localhost:3000"
echo "Ollama API: http://localhost:11434"
echo ""
echo "使用说明:"
echo "1. 将你的内容放入 data/your_content/ 目录"
echo "2. 调用导入 API: curl -X POST http://localhost:8080/ingest"
echo "3. 开始测试查询: curl -X POST http://localhost:8080/query"
通过本附录提供的环境,你可以:
- 快速验证内容在生成引擎中的表现
- 优化内容策略前进行低成本测试
- 调试结构化数据和语义标记
- 建立持续集成中的 GEO 质检流程
将此环境纳入你的全栈工具链,将显著提升 GEO 优化的效率和准确性。
