用 LlamaIndex 徒手撸一个「图片也能看懂」的私有知识库问答系统
·
一、为什么要“多模态 RAG”?
传统 RAG 只能啃文本,现实里却充斥着扫描件、截图、PDF。
今天我们把 OCR 塞进经典 RAG 流水线,让大模型「先看见、再回答」。
二、最终效果
- 打开浏览器,拖拽一张截图 → 自动识别文字 → 秒级建库
- 直接问图里的内容,模型给出带来源文件名的答案
- 支持多文件、多集合,管理员可随时切换知识库
- 全程 HTTP 接口 + Web 聊天,前后端分离,可内网部署
三、技术栈(全部开源)
| 模块 | 选用方案 | 说明 |
|---|---|---|
| LLM | DeepSeek / Moonshot | 兼容 OpenAI-API,无需翻墙 |
| Embedding | BAAI/bge-small-zh | 中文语义足够,100 MB 级 |
| 向量库 | Milvus Lite | pip install milvus,零配置 |
| OCR | Umi-OCR v2 | 离线、免费、开箱即带 HTTP 服务 |
| 前端 | Chainlit | 一行命令挂载到 FastAPI |
| 框架 | LlamaIndex | 胶水层,10 行代码完成 RAG |
四、30 分钟部署手册
① 环境准备
# 1. 创建虚拟环境
python -m venv venv && source venv/bin/activate # Windows 用 activate.bat
# 2. 一键安装依赖
pip install -r https://raw.githubusercontent.com/yourname/mini-ocr-rag/main/requirements.txt
② 拉起 Milvus(CPU 版)
docker run -d --name milvus -p 19530:19530 milvusdb/milvus:v2.3.10-lite
③ 拉起 Umi-OCR(Windows / Linux 均提供绿色包)
# 绿色包下载后双击 Umi-OCR.exe
# 默认会启 127.0.0.1:1224,可改端口
④ 配置环境变量
在项目根目录新建 .env,照抄改 key 即可:
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxx
MILVUS_URI=http://127.0.0.1:19530
OCR_BASE_URL=http://127.0.0.1:1224
OCR_DOWNLOAD_PATH=./data/ocr
⑤ 启动服务
python app.py
# 浏览器自动打开 http://localhost:8080
上传文件 → 输入集合名 → 点 Build Index → 跳到 /chainlit 开聊!
五、核心代码解读
1. OCR 一行函数
def ocr_image_to_text(path: str) -> str:
b64 = base64.b64encode(Path(path).read_bytes()).decode()
r = requests.post(f"{OCR_URL}/api/ocr",
json={"base64": b64, "options": {"data.format": "text"}},
headers={"Content-Type": "application/json"})
return r.json().get("data", "")
2. RAG 打包类
class TraditionalRAG:
async def create_index(self, files: list[str], collection: str):
docs = []
for f in files:
text = ocr_image_to_text(f) if Path(f).suffix in IMG_EXT else ocr_image_to_text(f)
docs.append(Document(text=text, metadata={"file": Path(f).name}))
vector_store = MilvusVectorStore(uri=MILVUS_URI, collection_name=collection, dim=512)
return VectorStoreIndex.from_documents(docs, vector_store=vector_store)
3. FastAPI + Chainlit 胶水
app = FastAPI()
mount_chainlit(app, target="chat.py", path="/chainlit")
@app.post("/upload")
async def upload(files: list[UploadFile], collection: str = Form(...)):
saved = [save_file(f) for f in files]
await TraditionalRAG().create_index(saved, collection)
return {"msg": "indexed", "count": len(saved)}
六、常见坑 & 秒解方案
| 现象 | 原因 | 一句话解决 |
|---|---|---|
| 上传后一直“Indexing” | 单线程阻塞 | 把 create_index 扔线程池 asyncio.to_thread |
| OCR 返回空 | 图片太大 | Umi-OCR → 全局设置 → 内存限制调到 4096 |
| Milvus 连不上 | 端口未暴露 | docker run -p 19530:19530 别漏 -p |
| 中文乱码 | 系统缺字体 | Linux apt install fonts-noto-cjk |
七、下一步可以玩的花活
- 多集合切换:把
collection做成下拉列表,存到 Postgres - 权限系统:Chainlit 自带
@password_auth_callback,5 行代码加登录 - 分段摘要:LlamaIndex
SentenceWindowNodeParser让长图也能精准定位 - 离线 LLM:换上
chatglm2-6b+xinference,彻底断网运行
八、总结
今天我们把「OCR → 向量化 → 检索 → 生成」四个步骤压缩成 2 个文件、不到 200 行代码,不挑硬件、不挑系统、不挑网络。
更多推荐

所有评论(0)