如果你被一堆“文档太多、答案全靠搜”的项目缠住,那你大概率已经在评估 RAG 了。用Phidata + PgVector搭一个智能检索问答助手,是我最近在多个内部工具型项目里反复用过、也踩过不少坑之后,觉得最值得推荐的一条路线。它不像从零写那一套加载、切块、入向量库、召回、再喂给模型的流程那么繁琐,也不像某些重框架那样上了就得买全家桶。Phidata 天然把 Agent、知识库、记忆、工具调用串在一起,PgVector 则让原本很重的一套向量基础设施直接塞进你已经在用的 PostgreSQL 里,运维和心理负担都小很多。
这篇文章我会从最基础的原理讲起,手把手带你走一遍环境准备、文档加载、Agent 问答、记忆与工具扩展、生产环境调优,再到常见问题排查。不管你之前有没有接触过 RAG,跟着这套思路走,应该都能在一天内把第一个能回答专业文档问题的助手跑起来。如果你已经是老手,重点看第 4 节和第 5 节,那里面有一部分是我在调检索质量和处理并发时总结的实操心得。
1. 先想清楚:RAG 助手的本质是什么,为什么这套组合合适
1.1 RAG 不是简单的“向量搜索 + 大模型”
很多人把 RAG 想得太简单,以为就是把问题拿去搜一搜、再把搜到的内容拼到 Prompt 里。实际上一个完整的 RAG 链路至少包含:文档加载、文本切块、向量化、向量存储、检索召回、上下文组织、生成回答。任何一个环节做得糙,最终答案都会翻车。
举个例子,假设你要做一个“产品说明书问答助手”。用户问“发票丢了还能保修吗”,如果只是按关键词搜,系统可能把“保修条款”“发票要求”两个片段割裂地搜出来,然后模型拼出一句“可以保修”但没提示需要什么凭证。真正合格的 RAG 要能把“保修需要发票”“发票丢失可提供购买记录”两段上下文都召回,再让模型给出有条件的回答。这要求的不只是向量数据库跑得通,更是分块粒度、召回数量、Prompt 结构之间的配合。
Phidata 聪明的地方在于,它没有把 RAG 拆成一堆必须自己拼装的零件,而是把“知识库 + 向量库 + Agent”作为一等公民封装好了。你关心的不是“这一步调哪个接口”,而是“我的 Agent 应该怎么回答问题”。这种思维方式的转变,对后期维护和功能扩展帮助非常大。
1.2 Phidata 的价值:从“检索脚本”到“Agent”的跨越
Phidata 最早出圈是因为做 Agent 开发很顺手,后来它的知识库抽象也越来越成熟。核心组件大概是这几块:
- Agent:对话主体,负责理解用户输入、决定要不要查知识库、组织回答。
- KnowledgeBase:统一管理文档的加载、分块、嵌入、重载逻辑。
- VectorDB:检索后端接口,PgVector、专用向量库、内存向量库都可以接。
- Memory:短期会话记忆和长期用户记忆,能存到 Postgres 里。
用这套抽象,代码写起来非常符合直觉。我这里说的是它的真实用法,不是概念预期。以我最早接触的早期版本为例,环境变量、命令名、类名都改过好几次,所以你看文档时如果发现某个模块路径和网上文章对不上,别慌,以你装的那个版本的官方示例为准。Phidata 的快速迭代是它活跃的体现,但也确实坑了一批照抄老教程的人。
1.3 为什么用 PgVector 而不是一个独立向量库
市面上的向量数据库不少,各有各的优点。但落到企业内部的“文档问答助手”这种场景,我越来越倾向于 PgVector,理由很实在:
- 运维简单:大多数团队已经有 PostgreSQL,再往里面装一个扩展,比单独维护一套 Milvus 或 Weaviate 轻量太多。
- 事务与备份:业务数据、用户对话记录、向量数据放同一套数据库,可以用统一的备份、权限、审计机制。分开两套系统,备份恢复也不得不同时考虑两边。
- 元数据过滤方便:向量检索时经常要按文档类型、部门、时间范围过滤,PostgreSQL 的常规字段和向量索引可以写在同一条 SQL 里,不用在业务代码里做两段式拼接。
- 数据量适中够用:对“几十万篇文档”“百万级 chunk”这个量级,PgVector 配合 HNSW 索引完全能扛住。真正到了亿级向量再迁移也不迟,没必要一开始就上重型武器。
2. 环境准备:把 Phidata 和 PgVector 跑起来
2.1 安装依赖并准备 PostgreSQL
在开始之前,先确认你的机器上有 Python 3.9 以上版本。我建议直接建一个干净的虚拟环境,避免跟系统 Python 或其他项目的包互相污染。
# 创建并激活虚拟环境 python3 -m venv .rag-env source .rag-env/bin/activate # 安装 Phidata pip install -U phidata接下来要解决 PostgreSQL。如果你本地本来就有,只需要确认版本在 14 以上,因为 pgvector 对新版本支持更平滑。如果没有,我个人最省事的方式是用 Docker 起一个带 pgvector 的实例:
docker run -d \ --name pg-rag \ -e POSTGRES_USER=ai \ -e POSTGRES_PASSWORD=ai \ -e POSTGRES_DB=ai \ -p 5532:5432 \ pgvector/pgvector:pg16这里把宿主机端口映射到 5532,是为了避免和本机已有的 PostgreSQL 冲突。如果你本机没有装过 PostgreSQL,直接映射 5432 也没问题。启动后,建议用 psql 验证一下扩展:
docker exec -it pg-rag psql -U ai -d ai -c "CREATE EXTENSION IF NOT EXISTS vector;"如果你用的是自己安装的 PostgreSQL,也一定要先执行这条CREATE EXTENSION,否则后续建表会直接报“type vector does not exist”。
2.2 设计数据库连接串
Phidata 内部的 PgVector 通过 SQLAlchemy 连接 PostgreSQL,连接串格式一般是:
DB_URL = "postgresql+psycopg://ai:ai@localhost:5532/ai"拆开来看:postgresql+psycopg表示用 psycopg 驱动,后面的ai:ai@localhost:5532/ai就是用户名、密码、地址、端口、数据库名。我用的是 psycopg,因为 Phidata 在当前版本里对它的支持最顺,比 psycopg2 和 asyncpg 少一些序列化方面的小问题。你要是遇到驱动缺失,先跑一句pip install "psycopg[binary]"。
连接串里的信息不是死的,端口、密码改成你自己的即可。测试连接最快的方法不是写 Python,而是用 psql 连一下:
PGPASSWORD=ai psql -h localhost -p 5532 -U ai -d ai -c "SELECT 1"能返回 1,说明连接没问题。这一步值得做,因为过后你分不清到底是代码问题还是数据库没起来。
2.3 准备第一批文档
环境就绪后,需要一个测试文档。最方便的是一份 PDF,任何产品手册、内部制度文件都行。如果没有现成的,也可以先准备一份 txt 或 markdown 文件,这样后面验证代码时不用等待太久的格式解析。
我自己通常更喜欢用本地文件起步,因为加载流程可控,不会像 URL 加载那样受到网络、超时、页面结构变化的影响。把这份文档放在项目目录下的data/目录,命名为manual.pdf,后面代码直接指向它。
3. 实现核心:让助手针对文档回答问题
3.1 用 Phidata 加载知识库并构建检索型 Agent
这是整套方案里最关键的一步。下面这段代码,是能跑起来的最简形态:
from phi.agent import Agent from phi.knowledge.pdf import PDFKnowledgeBase from phi.vectordb.pgvector import PgVector DB_URL = "postgresql+psycopg://ai:ai@localhost:5532/ai" # 1. 定义知识库:本地 PDF + PgVector 作为向量检索后端 knowledge_base = PDFKnowledgeBase( path="data/manual.pdf", vector_db=PgVector(table_name="pdf_documents", db_url=DB_URL), ) # 2. 首次运行时加载文档,之后注释掉这行 knowledge_base.load(recreate=True)load(recreate=True)的意思是:重新加载文档、重新切块、重新生成向量,并把表结构重建。第一次运行时用recreate=True很合适,因为表还不存在。但之后每次加载都recreate=True会导致全量重建,如果文档没变化,建议改成knowledge_base.load(),这样可以基于主键去重更新,节省大量时间。
加载完成后,再来定义 Agent 本体:
agent = Agent( model=Ollama(id="qwen2.5:7b"), knowledge_base=knowledge_base, search_knowledge=True, add_references=True, instructions=[ "你是企业内部文档顾问,只能依据用户提供的文档回答。", "如果知识库中没有明确答案,直接告诉用户不清楚,不要编造。", "回答时要引用文档中对应的条款或页码。", ], )search_knowledge=True是让 Agent 在收到用户问题后先去知识库检索;add_references=True是在回答里附上来源片段,这个对排查幻觉非常有帮助。
注意上面我用了Ollama作为模型来源,这是另一个开源工具,负责在本地跑开源模型,不需要联网调云服务。关于模型这块,我在下一节展开说明。
3.2 模型选型:本地模型还是云端模型
RAG 助手除了检索,还需要生成回答,这一步依赖大模型。如果你是个人学习或者数据敏感,我建议先用 Ollama 跑本地开源模型做验证。安装 Ollama 后拉取一个模型:
ollama pull qwen2.5:7b ollama pull nomic-embed-text然后回到 Python,模型部分这样写:
from phi.model.ollama import Ollama agent = Agent( model=Ollama(id="qwen2.5:7b"), ... )嵌入模型也要单独指定,保证 Phidata 在用同一个模型做向量化:
from phi.embedder.ollama import OllamaEmbedder knowledge_base = PDFKnowledgeBase( path="data/manual.pdf", vector_db=PgVector(table_name="pdf_documents", db_url=DB_URL), embedder=OllamaEmbedder(model="nomic-embed-text", dimensions=768), )dimensions必须和嵌入模型输出的向量维度一致。不同的嵌入模型,维度可能完全不同,比如有的是 768 维,有的是 1024 维、1536 维。如果换成另一个嵌入模型但维度还写 768,启动时就可能报向量维度不一致的错误。
如果你所在环境允许使用云端模型 API,只要把Ollama替换成对应厂商在 Phidata 里的模型类即可,其余代码不用动。这也是 Phidata 的一个好处:模型层完全隔离,业务代码不需要大规模修改。
3.3 运行并验证第一个 RAG 问答
把上面代码保存为agent.py,直接运行:
python agent.py如果一切正常,Phidata 会启动一个交互式会话。这时你就可以在控制台里提问了。我刚才那个“发票丢了还能保修吗”就是很好的测试问题。第一次跑可能有点慢,因为本地模型要加载到内存。耐心等一下,回答质量会随着后续提问逐渐稳定下来。
除了控制台,Phidata 还提供了一套基于 Streamlit 的界面。如果你只想快速可视化,可以再装一个 Streamlit:
pip install "streamlit"然后在代码里加几行,把 Agent 暴露成 Web 聊天界面。这一步对非技术同事验收特别有效,大家能直观看到这个助手真的能回答问题。我个人建议尽早让业务同事参与测试,因为他们提出的真实问题,往往比你自己脑补的测试问题更能暴露知识库的盲点。
4. 进阶:给助手加记忆和工具,从问答走向 Agent
4.1 用 PgMemoryDb 实现会话记忆与长期记忆
第一个版本能回答问题,但从产品角度看还不够,因为每次会话都是独立的。用户上一轮说了“我是华东区的”,下一轮问“我们区的保修政策是什么”,Agent 应该记得“华东区”这个上下文。这就要给 Agent 加记忆能力。
Phidata 里有专门的 Memory 抽象,而且可以接同一个 PostgreSQL:
from phi.memory.db.postgres import PgMemoryDb agent = Agent( model=Ollama(id="qwen2.5:7b"), knowledge_base=knowledge_base, search_knowledge=True, add_references=True, memory_db=PgMemoryDb(table_name="agent_memory", db_url=DB_URL), add_history_to_messages=True, )add_history_to_messages=True会在每次请求时把历史消息一起带给模型,实现多轮对话的连贯性。同时,PgMemoryDb把会话记录写入 PostgreSQL,重启应用后记忆还在。这比内存存储可靠得多,也是我一直强调“用同一套数据库”的原因:业务数据、向量、记忆全在一个地方,备份一次搞定。
4.2 通过工具调用扩展 Agent 的边界
RAG 解决的是“从文档里找答案”,但很多业务场景不只有文档。比如用户问“现在几点”“帮我算一下这批货的总价”,这些都不在文档里,Agent 需要调用外部函数。Phidata 的 Agent 支持把 Python 函数直接注册为工具。
我举个简单例子。假设你要让助手能根据文档内容 + 当前时间给出更准确的答复,可以这样:
from datetime import datetime def get_current_time(): """返回当前服务器时间,格式为 YYYY-MM-DD HH:MM:SS""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") agent = Agent( model=Ollama(id="qwen2.5:7b"), knowledge_base=knowledge_base, search_knowledge=True, tools=[get_current_time], show_tool_calls=True, )show_tool_calls=True会把模型决定调用哪个函数的过程显示出来。这非常有用,因为它暴露了模型的内在推理链路。如果你发现模型什么问题都先去调函数、而不是先查知识库,说明 Prompt 里的工具使用说明需要调整,或者工具描述写得太宽泛了。
工具调用的本质,是把“检索文档”和“执行计算”这两件不同的事分开:文档检索走知识库,实时信息走函数。这样职责清晰,后期也容易加权限控制,比如某些函数只允许特定角色调用。
4.3 多 Agent 分工:把问题拆解得更细
单 Agent 能处理不少任务,但碰到复杂业务,一个 Agent 包打天下会越来越笨拙。Phidata 支持把多个 Agent 组合成 Team,不同 Agent 负责不同专业领域。
举个例子,假设知识库里有产品手册,也有售后政策。你可以建两个 Agent:
- 产品Agent:专注回答产品参数、使用方法。
- 售后Agent:专注保修、退换货政策。
再建一个“调度 Agent”,负责理解用户问题、决定转发给哪个子 Agent、最后汇总结果。这种架构的好处是每个 Agent 的知识库更聚焦,Prompt 也更短,回答质量更稳定。代价是系统复杂度上升,不建议第一版就上,先让单个 Agent 把主流程跑通再说。
5. 生产环境调优:检索质量与稳定性
5.1 分块参数:chunk_size 与 overlap 怎么调
RAG 回答质量很大程度上取决于文本分块。分块太粗,一段里混了多个主题,检索时召回的内容不聚焦;分块太细,语义被切碎,模型拿到的上下文支离破碎。
Phidata 默认加载文档后会把文本切分成小块,常见的默认值大约在 300 字符左右、重叠 60 字符左右。我实际用下来的经验如下:
| 文档类型 | 建议 chunk_size | 建议 overlap | 理由 |
|---|---|---|---|
| 产品手册/制度文件 | 300-500 | 50-80 | 条款相对独立,保留些上下文即可 |
| 论文/长叙述文档 | 500-800 | 80-120 | 语义跨度大,需要更长上下文 |
| 对话/FAQ | 200-300 | 20-40 | Q&A 结构紧凑,分块太大会混入无关Q |
你可以用一个小脚本打印切块后的片段,直观感受分块效果。如果看到一块里混杂了两个完全无关的主题,就该调小 chunk_size;如果发现一个完整的答案被拦腰截断,就该调大 chunk_size 或增加 overlap。
5.2 向量索引:HNSW 与参数调优
PgVector 支持两种索引:IVFFlat 和 HNSW。IVFFlat 需要先有足够数据才能建索引,而且要指定聚类中心数量,不够灵活。HNSW 则是一种近似最近邻的图索引,查询速度快,构建参数相对少,更适合大多数应用场景。
建索引的 SQL 大概是这样的:
CREATE INDEX ON pdf_documents USING hnsw (embedding vector_cosine_ops);分片是 Phidata 自动建的,但索引策略可以自己控制。如果你发现检索变慢或者召回质量下降,可以先检查索引是否生效:
EXPLAIN ANALYZE SELECT * FROM pdf_documents ORDER BY embedding <=> '[向量]' LIMIT 10;注意vector_cosine_ops是针对余弦距离的操作符类,如果你用的是欧氏距离,要换成vector_l2_ops。Phidata 默认的检索方式通常与余弦距离保持一致,所以一般用vector_cosine_ops就行。
5.3 数据库连接与并发处理
上线之后,多个用户同时提问,每个问题都会触发知识库检索,PostgreSQL 连接数可能瞬间被打爆。SQLAlchemy 的连接池默认值不一定够用,建议根据并发量调整。
Phidata 的 PgVector 底层使用 SQLAlchemy,所以在连接串里加查询参数可以控制连接池大小:
DB_URL = "postgresql+psycopg://ai:ai@localhost:5532/ai?pool_size=10&max_overflow=20"pool_size=10表示保持 10 个连接,max_overflow=20表示高峰时可以临时扩充到 30。如果你的机器资源有限,这个数字不要调太大,否则连接风暴会反过来拖垮数据库。
还有一点容易被忽略:定期重载知识库。文档更新后,旧向量还留在表里,不重载的话检索到的可能是过期内容。你可以写一个定时任务,在凌晨低峰期调用knowledge_base.load(),用增量更新方式同步变化。配合日志记录,哪天回答出错了,也能清楚到底是哪份文档里的旧内容导致的问题。
6. 常见问题与排查技巧实录
6.1 加载文档之后,检索结果总为空
这是新手最容易遇到的问题。可能的原因有三个:
- 表和索引没建成功:检查
pdf_documents表是否真的存在,SELECT COUNT(*) FROM pdf_documents;看有没有数据。 - 向量化失败:如果 embedding 模型没配好,加载时不会报错,但库里全是空向量。查看加载日志,出现 embedding 相关警告就说明有问题。
- 检索条件过于严格:用户的提问方式和文档原文差异太大。先搜一个文档里的原文片段,如果原文都搜不到,那一定是链路问题;如果原文能搜到,说明又是语义匹配的调优问题。
6.2 更换 embedding 模型后维度不一致
之前说过,不同 embedding 模型输出的向量维度不同。如果你之前用的是 768 维,后来换成了 1024 维的模型,直接跑会报维度错误。这时需要把对应表删掉重建,或者先TRUNCATE再重新load(recreate=True)。
更稳妥的做法是:从第一天起就把 embedding 模型固定下来,不要在生产环境频繁切换。升级模型前先在测试环境完整验证一轮,确认检索质量确实提升了再动手。
6.3 回答不引用文档,感觉像在瞎编
我在第 3 节提到add_references=True,这一步只是把来源附在回答里,但 Prompt 里的 Instructions 才是决定模型“要不要引用文档”的关键。我在项目里经常给 Agent 加这样一条:
当你引用文档内容时,必须标明来源文件的名称和对应章节。 如果用户问你一个你无法从文档中确认的问题,请回答“该问题在现有文档中未找到明确说明”。这样能大幅减少幻觉。你还可以在 Agent 里打开详细日志,把检索到的片段打印出来,确认模型到底看了哪些内容。很多时候回答不准,不是模型不行,而是检索回来的内容里确实没有正确答案。
6.4 PostgreSQL 权限不足,无法创建扩展
Docker 方式启动的 PostgreSQL 一般没问题,但你用已有实例时,可能遇到当前用户没有超级权限,导致CREATE EXTENSION vector失败。解决方法是切换到超级用户,或者让管理员先创建好扩展,再给业务用户授权。不要在一个共享数据库里用超级用户跑日常业务,那是运维事故的温床。
6.5 我踩过几次坑之后想分享几个心得
第一,知识库的内容质量比模型大小重要得多。我见过有人换了更大的模型,回答反而变差了,原因就是文档本身过时、矛盾、格式混乱。先把文档整理干净,比换模型更划算。
第二,先小步验证再扩大规模。不要第一天就把一百份文档全塞进去,先拿一份文档打通链路,确认每一个环节都正常,再逐步加文档。全量加载之后如果回答质量下降,你也知道是新加的哪批文档出了问题。
第三,把检索日志当成产品监控来做。每次问答都记录一下“检索了哪些片段、命中了哪份文档、得分是多少”。有了这些日志,用户反馈“回答不对”时,你能快速定位是检索没召回,还是模型没理解。这一步在早期省了我大量时间。
第四,注意知识库的权限边界。企业内部文档常常混杂着公开信息和敏感内容,向量检索本身不会区分权限。如果系统要给不同角色开放不同范围的知识库,一定要在应用层做好文档级别的访问控制,不要让向量表裸奔在公网环境。
最后的一点个人体会
这套 Phidata + PgVector 的组合,我最喜欢的地方是它没有逼着你把技术栈拆得七零八落。一个 Python 环境、一个 PostgreSQL 实例,就能把一个智能问答助手从原型带到生产。比起那些动辄要部署三四个服务的方案,这套简化掉的运维复杂度会变成你后续迭代的信心来源。我在实际使用中最大的感受是:RAG 的技术门槛没那么高,真正难的是把检索质量调稳、把知识库内容管好,以及让 Agent 在“知道”和“不知道”之间诚实切换。如果你正要开始做智能 RAG 助手,不妨先按这篇文章跑通最小闭环,再根据你自己的文档特点去调整分块和检索参数。接下来你还可以往多模态文档、结构化知识图谱、自动知识库更新这些方向去扩展,每一步都有 Phidata 的组件在背后撑着,不会让你推倒重来。