
腾讯最近发了 ima 的架构文章我仔仔细细读了两遍。第一遍感叹工程做得很扎实第二遍直接来了兴趣——这东西拆开看核心链路其实没有超出 RAG Agent 的范畴。那么问题来了能不能照着它的设计思路自己在本机搓一个 ima.copilot我花了两天时间把这事干成了整个过程整理了这篇文章给想搭个人知识库助手的朋友做个参考尤其适合那些对 RAG、向量检索、Agent 编排有一点概念但没完整跑通一条链路的开发者。1. 腾讯那篇架构文章到底说了什么先说结论ima.copilot 不是一个单点的问答工具而是一整套以个人知识库为核心的 AI 工作台。它的典型使用方式是你把 PDF、网页、对话记录、笔记丢进去然后用自然语言提问它带着引用告诉你答案还能帮你写东西、做脑图、整理知识结构。从架构文章里能梳理出来的东西大致是这几层接入层PC 端、移动端、小程序/微信内入口所有入口共用一套后端能力。知识接入层支持多格式文档的解析和清洗把非结构化内容转成可检索的结构化切片。索引与检索层同时做了语义向量召回和关键词召回再加上重排保证检索质量。生成层基于 LLM 做回答生成答案里带上引用来源。Agent 编排层负责理解用户意图、拆解任务、决定要不要调工具、多轮对话怎么维护上下文。单独看任何一个模块都不新鲜向量数据库、RAG、重排、Agent 这些概念社区里天天在讲。真正有价值的是它把模块之间的数据流和控制流理得很干净。我照着这套逻辑在本地搭的时候最大的感受是官方架构解决的是规模化问题但单机版更需要解决的是性价比和闭环问题。有些模块可以砍有些模块不能砍砍了链路就跑不通。另外值得注意的一点是官方架构里的知识库不是简单的文件丢进去就完事它有明确的知识理解环节比如对文档做结构解析、对长文档做分块策略选择甚至可能包括表格抽取、OCR 等预处理。这也给我提了个醒本地版最不能偷懒的两个地方一个是分块一个是检索这两步决定整个系统能不能用。2. 本地版拆解组件选型与每一步的理由我搓的本地版严格遵循一条原则不追求高并发不追求多租户只追求一个人在一台电脑上能流畅地把导入知识 - 提问 - 带引用回答这条循环跑起来。所以很多组件我选择的是极简方案但每一步都先说清楚为什么这么选。2.1 文档接入与解析用规整的文件格式降低解析成本官方版要处理各种乱七八糟的格式比如扫描件、复杂排版、音视频转写。单机版完全没必要一上来就搞全套我先锁定三种最常用的格式Markdown 和纯文本直接读零成本。PDF用 pypdf 抽文本复杂排版的用 pdfplumber 兜底。Wordpython-docx 读取段落和表格。选型理由很简单解析不是本地版的核心价值正式内容大多有清晰的文本层不值得在这里投入太多精力。如果文件本身就是扫描件我宁愿直接跳过一个提示也不想引入 OCR 那套重型依赖。当然如果你的场景全是影印版 PDF那 OCR 是绕不开的后面我会提一句可以怎么加。2.2 分块这是最容易被低估的环节官方文章里没细说分块策略但所有 RAG 项目的经验都指向同一个结论分块大小决定了检索精度的上限。分太大语义混杂召回噪声高分太小语义不完整召回率低。我在本地版用的是递归字符分割优先按标题、段落这样的自然边界切。具体参数上块大小设成 512 个 Token重叠 64 个 Token。这个组合在大多数中文文档上表现比较均衡后面踩坑部分我再展开讲。2.3 向量化国产开源模型足够用向量模型我选了 BGE-M3。原因有三个第一它同时支持中文和英文效果在开源模型里属于第一梯队第二它除了生成稠密向量还能生成稀疏向量后面做混合检索可以直接用它第三它对显存要求不高CPU 也能跑只是慢一点。2.4 向量存储从 SQLite 到 Qdrant第一版我用的是 Chroma图省事。后来发现混合检索和自定义打分不太方便就换成了 Qdrant。Qdrant 支持稠密向量和稀疏向量并存也支持 filter对本地部署来说资源占用比 Milvus 小得多。你如果只是存几千个切片用 Chroma 或者 SQLite 加扩展也完全够但既然要做到照着官方架构走一遍检索层还是值得认真对待的。2.5 LLM本地推理和 API 我都留了接口我做了两层抽象默认接本地 Ollama 跑的 Qwen2.5-14B量化之后大概 9GB 显存家用卡就能带得动如果你机器带不动也可以一键切到云端 API。我想特别说明一下为什么选 14B 这个档位——太小了指令遵循能力不够生成引用格式的时候经常不听话太大了本机响应时间又扛不住。14B 是在质量和延迟之间比较让人安心的折中。2.6 Agent 编排不用 LangChain 全家桶官方架构里一定有 Agent 层但本地版我一开始用的是 LangChain 的许多组件后来发现没必要这么重。最后我换成了 LangGraph原因是它可以画状态机一样控制流程路由和工具调用都写在明面上出问题好排查。而且它和 LangChain 的组件能复用不冲突。下面把整体架构和官方版的对应关系拉个表格方便你对照模块官方 ima.copilot我的本地版方案接入端多端本机 Web 页面Streamlit文档解析多格式 OCRpypdf / python-docx / markdown分块智能分块递归字符分割512 Token重叠 64向量模型商用模型BGE-M3向量存储分布式向量库Qdrant 单机版混合检索语义 关键词 重排BGE-M3 稠密 稀疏向量 BGE-RerankerLLM腾讯混元Qwen2.5-14B可切换 APIAgent全套编排LangGraph 自定义状态机3. 手搓核心链路从目录解析到检索生成这一节是能直接拿去用的部分。我会把代码链路完整走一遍只保留关键代码逻辑说清楚。3.1 解析与入库先定义文档结构体from dataclasses import dataclass from typing import Optional dataclass class Chunk: doc_id: str # 文档唯一 ID chunk_id: str # 切片唯一 ID text: str title: Optional[str] None # 来源标题 page: Optional[int] None # 页码解析入库的流程分四步读文件 - 分块 - 向量化 - 写入 Qdrant。def ingest_file(filepath: str): # 1. 读文件 file_text, meta parse_file(filepath) # 2. 分块 chunks split_text(file_text, chunk_size512, overlap64) # 3. 向量化 dense_vecs bge_model.encode_dense([c.text for c in chunks]) sparse_vecs bge_model.encode_sparse([c.text for c in chunks]) # 4. 写入 Qdrant point_records [] for idx, c in enumerate(chunks): point_records.append({ id: c.chunk_id, vector: dense_vecs[idx].tolist(), sparse_vector: sparse_vecs[idx], payload: { doc_id: c.doc_id, text: c.text, title: c.title, page: c.page, source: filepath } }) qdrant_client.upsert(collection_nameknowledge, pointspoint_records)这里有个关键细节必须把text完整存进 payload。很多新手犯的错误是只在向量库里存了一个 ID查询到之后再去数据库里捞原文绕一大圈还容易丢数据。本地版的体量完全可以把原文直接放进去检索结果一步到位。3.2 检索混合召回 重排官方架构里检索一定不是单一向量检索。我实测过纯向量检索对包含关键词但不完全语义匹配的查询很弱比如用户问苹果公司的 CEO文档里写的是蒂姆·库克现任首席执行官向量检索没问题但如果文档里写的是Apple 公司 CEO中英文混杂向量就很容易跑偏。所以我做了两路召回一路用 BGE-M3 的稠密向量做语义检索。另一路用 BGE-M3 的稀疏向量配合 BM25 做关键词检索。两路各取 Top 20合并去重后再用 BGE-Reranker 给候选打分重排最后取 Top 5 作为上下文。def retrieval(query: str, top_k: int 5): # 两路召回 dense_hits qdrant_client.query_points( collection_nameknowledge, querybge_model.encode_dense([query])[0].tolist(), limit20, ) sparse_hits qdrant_client.query_points( collection_nameknowledge, querybge_model.encode_sparse([query]), usingsparse_vector, limit20, ) # 合并去重 candidates {} for hit in dense_hits.points sparse_hits.points: candidates[hit.id] hit.payload # 重排 pairs [(query, candidates[cid][text]) for cid in candidates] scores reranker.rank(pairs) sorted_ids [item.corpus_id for item in scores][:top_k] return [candidates[cid] for cid in sorted_ids]混合检索的代价是查询延迟明显上升但在本地单用户场景下多出来的几百毫秒是完全值得的。我后面会讲怎么用缓存把这个延迟降下来。3.3 生成引用怎么落地官方的答案带引用这一步在本地版里也很重要。做法是把重排后的切片按顺序编号拼进 prompt要求模型在回答时把 [1] [2] 这样的角标放在对应的句子后面并且禁止编造来源。def build_prompt(query: str, context_chunks: list[dict]) - str: context_text for idx, ch in enumerate(context_chunks, start1): context_text f[{idx}] {ch[text]}\n\n prompt f请根据提供的资料回答问题回答时在对应句子后标注来源角标如 [1]。 如果资料中没有相关内容直接说明资料中未找到相关信息不要编造。 资料 {context_text} 问题{query} 回答 return prompt生成的时候还要一个后置处理把模型输出的角标解析出来加一个可点击的引用块。我这边因为前端是 Streamlit直接在下面对应区域渲染一个来源板块就行。4. 实测两天后复盘检索不到、引用错误、延迟爆炸链路跑通只是第一步真正让我抓狂的是第二天的实测。我把几份真实的行业报告和产品文档灌进去问了二十几个问题暴露出来的问题一个比一个典型。4.1 召回质量差的根源竟然是分块第一个问题用户问某产品的市场渗透率趋势结果系统答非所问。查了半天发现文档里有好几页在讲市场——有的在讲技术市场规模有的在讲应用市场规模我用的 512 Token 固定分块把这些内容混在了一起语义被稀释了。解决方式是在分块之前先对文档做一次结构感知。我加了一个轻量做法优先按 Markdown 标题层级切分把标题和正文绑定成一个块。没有标题的 PDF就按段落边界切段落大于 512 Token 的再强制切断。这其实就是在模仿官方架构里知识理解的简化版。还有一个小技巧把每块的第一句当成标题候选在向量化时用标题 正文拼接检索时标题权重自然就上去了。这个改动让我的召回 Top 5 准确率提升非常明显。4.2 只靠向量检索问原名就沉默另一个让我印象很深的问题是资料里通篇写腾讯混元大模型用户问Tencent Hunyuan 的技术特点语义向量匹配不到因为没有共同 Token也没有很强的语义关联。这也是我把稀疏向量召回加进来的直接原因。加了之后这类中英文混杂、专有名词变体的查询成功率明显改善。关键词召回不是可选项是必选项。常见的做法里还有用 Jieba 分词加 BM25 的但既然 BGE-M3 直接给出了稀疏向量就不需要额外的分词器和 BM25 了Qdrant 原生支持 sparse vector 查询少维护一套索引。4.3 引用错误模型总想善意地补一句第三个问题出现在生成环节。明明检索回来的内容里没有同比增长 30%这句话模型还是会顺着语气把它补进去。这在 RAG 系统里太常见了官方架构里应该也有防幻觉的手段我的做法是两条prompt 里强制约束没有原文依据的信息一律不写。对引用角标做后端校验模型如果标了 [3]但 [3] 的原文里没有对应表述就把这个句子标记为无引用并提示用户。这个后置校验很管用因为它把模型是否撒谎变成了引用是否能对上原文后者是可以程序化检查的。4.4 延迟爆炸与本地优化的三板斧14B 的模型在单卡上推理速度本来就有限再叠加重排一个问题从发起到返回经常要 15 秒以上。我做了三件事把交互体验拉回正常Query 缓存同样的 query 在短时间内不重复计算直接用上一次的答案。向量模型常驻显存BGE-M3 和 Reranker 都常驻不让它们在推理前后加载卸载。结果缓存对每个 query 的 Top 5 检索结果做哈希缓存检索直接走缓存只让 LLM 重新生成。实际收益重复问题的响应时间从 15 秒降到 2 秒以内。新问题的响应时间也从 15 秒降到 8 秒左右主要省在没有了模型加载和冷启动的损耗。5. 再进一步把官方 Agent 调度思路搬进本地版官方架构文章里Agent 的部分让我最兴奋。它不只是简单地检索然后生成而是会根据问题的类型决定走哪条处理路径。我照着这个思路在本地版里加了一个轻量级的调度层用的 LangGraph。5.1 意图路由先判断走哪条路我把常见问题分成四类知识库问答问题涉及到用户导入的文档走标准 RAG 链路。闲聊/通用问答不涉及知识库直接让 LLM 自由回答。结构化查询例如统计这是哪年哪月的数据需要调数据库或者做聚合走工具调用。多轮追问基于前一轮的上下文继续需要做查询改写。路由本身不复杂一个小型分类模型或者让 LLM 用 JSON 输出意图标签都行。我用的后者因为省事而且大模型在这种任务上准确率很高。5.2 多轮对话与查询改写官方场景里用户不可能只问一个问题。你问完Q1再问那它呢如果没有多轮上下文第二个问题就是废的。我在 LangGraph 里加了一个节点把最近三轮对话历史拼进去让模型先判断当前问题是否指代不完整如果是就改写成一个可独立检索的问题再进入 RAG。这个节点花了我很少的时间但对体验的提升尤其明显属于性价比最高的改动。5.3 工具调用把知识库外的信息也接进来本地版目前支持三个工具当前时间/日期回答最近三个月这种相对时间时不用硬编码。网站搜索当文档库里没有相关内容时可以走搜索接口拿实时信息然后把结果和知识库内容一起交给 LLM 综合回答。本地文件系统查询允许模型读取某个目录下的文件列表辅助回答你有没有我上个月的会议记录这类问题。这些工具接入的难度都不大关键是要注意权限边界。我特地限制了搜索和文件读取的范围避免模型把电脑里的其他目录扫了个遍。官方架构里对这种控制一定有更成熟的做法但本地版至少要做到不在不必要的时候拿到过多权限。5.4 本地版做到现在能到什么程度说实话单机版在单人单库的体量下已经具备了官方 ima.copilot 的绝大部分核心体验带引用的问答、基于个人文档的总结、简单的多轮会话以及一点工具调用的能力。差距主要在几个地方工程架构官方是微服务化、分布式架构可以支撑海量用户和超大知识库本地版是单体进程百 GB 级别的知识库肯定扛不住但个人文档量级完全没问题。知识理解深度官方有非常复杂的文档解析与知识提炼链路我做的是轻量简化。移动端体验官方能微信里直接答这个本地版做不到也没必要做。6. 一点实操后的个人体会手搓这个本地版最大的收获是对架构这个词有了更具体的认知。官方文章的每一层设计都不是摆设接入层决定了产品形态的边界检索层决定了回答质量的上限Agent 层决定了系统的智能程度上限生成层决定了用户的信任度。我照着它做一遍等于把 RAG 系统的每个坑都踩了一遍比看十篇教程都有用。最后分享一个小技巧你在按类似思路搭自己的版本时先别急着上框架先用最原始的方式把链路跑通——读文件、切块、算向量、检索、拼 prompt、调 LLM。链路通了之后再逐步引入 Qdrant、LangGraph 这些工具。这样每次出现问题你能快速知道是哪一层的事而不是在一堆框架抽象里大海捞针。我的这套实现也没有用任何需要付费的东西全部是开源组件加本地模型跑起来之后的惊喜感还是很足的。后续我准备再往里加个批量导入微信读书笔记的功能把个人知识库和阅读记录连起来到时候再单独写一篇。