信息爆炸这个词说了十几年真正做情报系统的人才懂它有多疼。每天几十个RSS源、上百条行业快讯、一堆PDF报告和内部文档全堆在面前你要的是某竞品最近一个月在华南区的投放变化结果翻了几十页搜索结果依然拼不出一个完整答案。这就是我重构这套AI情报系统的原始动机。核心答案很简单把大模型当大脑把Vector Engine当记忆体让语义检索和生成能力各司其职形成一个能自动采集、理解、关联、产出洞察的情报闭环。这篇文章不是一个Demo演示而是一整套可落地的架构方案。我会把系统拆分到模块级别讲清楚每个环节为什么这么做Vector Engine在里面的角色是什么混合检索怎么做重排序怎么搭以及我在调优过程中踩过的四个大坑。全文核心源码片段都会给到适合正准备做知识库、情报监控、企业搜索或智能问答系统的开发者直接参考。1. 重构前的问题为什么传统搜索方案顶不住了1.1 旧系统长什么样早期版本用的是最朴素的组合爬虫抓数据存入Elasticsearch再做关键词匹配配合一套规则分类器打标签。这套系统在数据量只有几十万条的时候勉强能用但问题非常明显。第一是语义泛化能力差。成本控制优化和降本增效明明说的是同一件事关键词搜索就是匹配不到。第二是长文本处理基本失效。一篇五千字的行业深度分析倒排索引只能拿片段去匹配无法从整体语义上判断它是否和查询相关。第三是告警噪音爆炸。规则写得太严就漏报写得太松就一天收到几百条无关推送。1.2 为什么必须换核心引擎市场上不是没有别的选择。Elasticsearch 8开始支持向量检索图数据库也能做关系挖掘但实际评估下来都有硬伤。ES的向量检索属于后加的插件能力在千万级向量规模下性能和稳定性都不如专业向量引擎而且它的核心倒排索引做精准关键词匹配确实很强但做语义相似度召回就是绕远路。图数据库擅长的是多跳关系查询比如A公司和B公司的股权关联但它解决不了saying同一件事的不同文本这种语义相似问题。向量引擎的思路更本质把文本语义映射到高维空间相近的信息自然聚在一起。我用Vector Engine做核心检索层看中的是它在亿级向量下的低延迟召回、HNSW索引的原生支持以及可以直接过滤时间、来源、标签这类结构化元数据的检索能力这让它非常适合情报这种强时效、多来源、大并发的场景。1.3 重构后的目标形态这次重构我不只想要一个更好的搜索引擎我想要的是一个接近全知全能的情报系统。全知指的是能把所有来源的信息统一收进来、统一向量化、统一关联全能指的是既能精确回答某条新闻说了什么也能回答某件事近期的发展脉络这种需要跨文档归纳的问题。整个系统的设计目标拆成五条单条数据从采集到可检索延迟控制在5分钟以内支持文本、图片OCR结果、音视频转写内容统一入库检索既要保语义召回也要保留关键词精确匹配能力最终生成的情报必须带引用来源不允许模型自由发挥系统能在百万级向量规模下保持稳定运行2. 核心选型Vector Engine凭什么成为情报系统的心脏2.1 先搞懂向量引擎到底解决什么问题一句话解释向量引擎它把一切非结构化数据变成数学向量然后在高维空间里做找邻居这件事。文本通过嵌入模型变成一长串数字这些数字的几何关系就代表着语义关系。苹果发布了新品和这家科技巨头推出了新设备在语义上是近邻它们的向量在空间里距离就很近。Vector Engine做的事情就是在海量向量里快速找到距离最近的TopK。它不关心你的向量怎么来的只负责高效存储和精准召回。情报检索正好是这种模式的典型场景数据实时进来、查询模式多变、对召回质量要求高。2.2 索引结构与关键参数向量检索里的核心数据结构是近似最近邻索引。我当时对比了Flat暴力计算、IVF倒排文件和HNSW层次化小世界图三种方案最终选了HNSW。原因很直接。Flat准确率最高但大规模数据下查询太慢只能当基准线用。IVF需要训练粗聚类中心聚类质量直接影响召回而且集群更新要重新训练。HNSW是一种图结构索引查询时从高层入口逐步下钻到底层每次只需要访问少量邻居节点在百万级以上数据里也能保持个位数毫秒级的延迟而且支持增量写入这非常适合情报数据不断涌入的场景。实际部署时我做了两套索引配置。热数据用HNSWM设为16efConstruction设为200efSearch设为100。这组参数在和我业务数据量匹配的测试集上召回率能到93%以上平均查询延迟在12毫秒左右。冷数据用IVF因为这类数据很少被实时查询压缩比更好可以省内存。2.3 为什么不做纯大模型方案有人可能会问直接把所有文本丢给大模型做总结不就行了还真不行。大模型的上下文窗口再怎么扩展也不可能装下千万级文档。更现实的问题是成本每来一条新信息就全量喂给模型做分析费用会迅速变成一个无底洞关键是它无法证明我引用的内容确实来自原始材料。这和人的记忆机制很像。大模型应该是大脑负责推理和表达而Vector Engine应该是长期记忆体负责记住所有原始信息。查询的时候先从记忆体里把相关的几个片段捞出来再交给大脑组织答案。这也是RAG检索增强生成的核心思想。这套架构下模型永远只处理跟问题相关的那一小批文档成本可控答案可溯源。3. 系统架构拆解一条采集-理解-召回-生成的完整链路3.1 五层架构总览整个系统我拆成了五层每层只做一件事层与层之间通过消息队列解耦。这样设计的好处是任何一层挂了都不会影响全链路而且每一层都可以横向扩展。采集层对接RSS源、开放API、网页爬虫、内部文档上传入口统一的采集网关做了频率控制和格式归一化处理层对原始内容做正文提取、指纹去重、标签抽取、摘要生成、文本切块、向量化存储层原始数据落对象存储结构化元数据落关系型数据库向量数据进Vector Engine检索层提供统一的混合检索接口同时走向量召回和关键词召回再做重排序应用层情报问答、定时简报、异动告警、专题分析每个应用都复用检索层接口处理层和检索层是最核心的两层。处理层决定了信息进去之后的理解水平检索层决定了用户提问时系统能不能找得准。3.2 数据流是怎么样走的一条新情报的完整旅程是这样的采集网关从某个RSS源抓到一篇行业新闻处理层先做正文提取去掉导航和广告信息然后算一个MinHash指纹发现和已有文档相似度只有0.3判定为新内容。接着抽取发布时间、来源、涉及的主体标签生成一段200字以内的摘要。然后进入切块环节文章被切成512字左右的块相邻块之间有64字的重叠防止关键信息正好落在切的缝隙里。每个块调用嵌入模型得到768维向量连同元数据一起写入Vector Engine。查询侧同理。用户输入一个问题系统先用一个轻量意图识别模块判断这是事实查询还是趋势归纳然后把问题向量化去Vector Engine里拿Top100候选再用BM25拿Top50关键词候选合并后做RRF融合最后用重排序模型把得分最高的15个片段取出来和原始问题一起拼成Prompt送给大模型。3.3 切块策略背后的细节切块这件事看起来简单其实对召回质量影响极大。块太小会丢失上下文比如苹果这种词单看一块根本不知道说的是水果还是公司。块太大又会让向量表征变得模糊因为一个块里包含太多主题嵌入模型很难取一个方向代表全部内容。实测下来512字加64字重叠这个配置在我的数据集上效果最好。重叠的64字是为了保证跨块的信息连续性比如一段讲到成本控制下一段开头是与之类似的做法如果没有重叠第二段的向量就会缺失重要的指代信息。还有一个容易忽略的细节对标题单独切块并加权重。标题往往是全文语义的浓缩我在向量化时把标题向量和正文首块向量做了加权融合标题权重设为0.3正文权重0.7这样能显著提升那些只看摘要就能命中的查询质量。4. 核心模块源码与关键实现4.1 向量化服务向量化服务是整个系统里最容易被低估的组件。它扛着所有文本转向量的压力同时还要保证向量模型版本可控、推理稳定。我把它做成一个独立的小服务通过HTTP接口对外提供向量化能力内部用了批处理来提升吞吐。import os from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel import numpy as np from typing import List # 这里用的是通用的文本向量化模型实际部署时换成你自己的模型路径 from sentence_transformers import SentenceTransformer app FastAPI() model SentenceTransformer(os.environ.get(EMBEDDING_MODEL_PATH, ./models/text_encoder)) class EmbeddingRequest(BaseModel): texts: List[str] class EmbeddingResponse(BaseModel): vectors: List[List[float]] app.post(/embedding, response_modelEmbeddingResponse) def embedding(req: EmbeddingRequest): # 模型在 GPU 上推理通常 64 条一批是最优吞吐 vectors [] batch_size 64 for i in range(0, len(req.texts), batch_size): batch req.texts[i:ibatch_size] vecs model.encode( batch, normalize_embeddingsTrue, # 必须归一化后续用内积近似余弦相似度 show_progress_barFalse ) vectors.extend(vecs.tolist()) return EmbeddingResponse(vectorsvectors)我用的是768维的通用中文文本向量模型归一化之后直接用内积当相似度省去余弦计算的性能开销。注意一个重要的点如果后续想换向量模型一定要把全部存量数据重新向量化不同的模型产出的向量不在同一个语义空间混用会直接导致召回效果崩盘。4.2 Vector Engine写入与检索Vector Engine的写入接口很简单但有一个容易忽略的坑每个向量除了ID和向量值一定要带上足够的元数据字段。情报检索场景永远需要过滤比如最近七天的新闻、某个来源、某个标签这些过滤条件如果在向量索引之外做效率会很差等于先把全量数据拿出来再筛一遍。所以我在写入时就把时间戳、来源、标签、文档ID全部作为可过滤字段存进去。from ve_engine import VectorEngineClient client VectorEngineClient( endpointyour-ve-endpoint, collectionintelligence_store, dimension768, index_typeHNSW, metricIP # 因为向量已经归一化用内积等价于余弦相似度 ) def write_documents(docs): docs: list of dict每项包含 id, vector, metadata rows [] for doc in docs: rows.append({ id: doc[id], vector: doc[vector], timestamp: doc[ts], # int 类型用于过滤 source: doc[source], # string 类型用于过滤 tags: doc[tags], # 数组类型用于标签过滤 doc_id: doc[doc_id] # 关联原始文档 }) client.upsert(rows) def vector_search(query_vector: List[float], top_k100, filtersNone): resp client.search( vectorquery_vector, top_ktop_k, filter{timestamp: {$gte: filters[start_ts]}, source: {$in: filters[sources]}}, output_fields[doc_id, timestamp, source, tags] ) return resp我最开始踩过一个坑把所有字段都塞进向量引擎包括一些不常用的大字段。后来发现向量的元数据也占内存字段越多每一条记录的内存开销越大索引构建速度也越慢。正确的做法是向量库只放检索用的小字段正文的完整内容留在对象存储里需要做生成的时候再按doc_id回源取。4.3 混合检索与重排序只用向量检索会漏掉一些场景。比如用户搜2025年大模型融资如果某篇正文里明确提到了具体金额和日期但整体语义上这篇主要讲的是技术路线向量召回时它可能排到很后面。这个时候BM25的精确词匹配就派上用场了。混合检索的融合策略我用的是经典的RRF公式。这个公式的好处是鲁棒性好不需要调权重它只看排名不看分数。然后为了进一步提升精度我用一个cross-encoder重排序模型对融合后的Top100再做一次精细打分。def rrf_fuse(vector_hits, keyword_hits, k60): Reciprocal Rank Fusion融合向量和关键词召回结果 scores {} for rank, doc in enumerate(vector_hits): doc_id doc[doc_id] scores[doc_id] scores.get(doc_id, 0.0) 1.0 / (k rank 1) for rank, doc in enumerate(keyword_hits): doc_id doc[doc_id] scores[doc_id] scores.get(doc_id, 0.0) 1.0 / (k rank 1) sorted_docs sorted(scores.items(), keylambda x: x[1], reverseTrue) return [doc_id for doc_id, _ in sorted_docs[:100]]重排序这一步被很多人忽略但它对最终情报质量的影响仅次于检索本身。向量召回到的文档大致相关但不代表精确对口。cross-encoder会同时把问题和一个候选片段一起编码判断两者是否真正匹配这个精度比双塔式的向量召回高一个档次。我用一个小规模的模型100条候选排序大约耗时200毫秒完全可以接受。4.4 情报生成的Prompt设计数据都备齐了最后一步是大模型生成。这一步最怕的是模型胡说八道所以我用了三层防护第一层明确告诉模型只能依据给定的参考片段回答第二层要求每个结论都标注引用编号第三层如果参考片段无法支撑问题必须明说无法确认。def build_analysis_prompt(question: str, contexts: List[dict]): context_block \n\n.join( f[片段{i1}] (来源:{ctx[source]}, 时间:{ctx[ts]})\n{ctx[content][:800]} for i, ctx in enumerate(contexts) ) prompt f你是一个情报分析助手。请基于以下参考片段回答问题不要使用片段之外的知识。 参考片段 {context_block} 用户问题{question} 要求 1. 每个结论后面用[编号]标注来源片段 2. 如果参考片段不足以保证答案完整在末尾注明部分信息无法确认 3. 输出结构一句话结论 要点展开 信息来源 return prompt很多人做RAG的时候只关心把文档塞进去不关心Prompt的约束力有多强。实测下来同样的检索结果严格的Prompt约束可以把幻觉率从18%降到4%以下这个收益是白捡的。5. 性能调优实录从召回翻车到稳定上线的过程5.1 第一次上线就翻车向量化服务内存被打爆第一版我把向量化服务做成了同步调用处理层每切好一块文本就同步发一次HTTP请求。数据量小的时候没事一旦采集层突然涌进几百篇文章切出来的块数量暴增模型推理请求排队服务内存直接打满然后OOM。排查过程还算顺利。看到监控里内存曲线在访问高峰期直线拉升马上就定位到问题是推理请求堆积。解决方案做了三件事入口加消息队列削峰把同步调用改成异步处理模型推理加批量合并最多攒够64条一起推理而不是一条条跑模型权重转为半精度FP16存储显存占用直接降了一半。5.2 召回率上不去的真正原因模型和链路都正常之后我用一套带标注的测试集测召回率发现只有62%。一开始怀疑是向量模型不够强换了好几个文本嵌入模型发现提升有限。后来把召回样本拉出来逐个看发现大量漏召回是查询太短、语义太泛导致的。比如用户查最近有什么大事这个query的向量本身就没有明确指向从高维空间里找邻居自然找不到让人满意的结果。解决方案是加了一个查询改写环节先把用户的原始问题丢给大模型改写成2到3个更具体的推测性查询比如最近一周的重大政策变化、头部公司的重要产品发布然后逐个去检索再融合结果。光这一步召回率从62%跳到了81%。5.3 相似内容淹没结果时间衰减和多样性重排数据多了之后又出现一个新问题一条重大新闻发布后各个渠道疯狂转载检索结果首页全是同一个事件的不同稿子。看起来召回率很高但信息冗余严重。第一招是时间衰减。同一篇内容最近发布的应该在排序上明显占优情报场景里时效性几乎是第一位。我在最终打分公式里加了一个时间因子分数乘上exp(-0.01 * 小时数)超过48小时的内容衰减得非常快。第二招是MMR多样性重排。每次选出一条结果后下一个候选不仅得分要高还要和已选结果在语义上不够相似这样保证同一个观点只出现一次。这两个策略配合之后用户实际感受到的信息密度提升了不止一倍同样的一个查询能看到不同角度的报道而不是被同一篇通稿刷屏。6. 常见问题与排查技巧实录6.1 脏数据导致的向量污染这是我最想提醒大家的一点。情报采集来的原始文本质量参差不齐有的带大量广告噪音有的是乱码。如果这些脏数据直接切块向量化它们会在向量空间里形成一个个语义孤岛污染附近的检索结果让相似内容召回率局部失真。处理办法是在处理层加了一道质量分闸门。每条文本切块前先算一个基础质量分根据正文长度、标点符号密度、重复字符比例来评估低于阈值的直接进垃圾箱不参与向量化。这个闸门救了我无数次强烈建议你们做类似系统时也加一道。6.2 中文内容同义不同形的误判中文情报里大量存在公司名简称、品牌词变体之类的情况。比如某公司全称、简称、股票代码指的是同一个实体但向量模型对它们的表征并不完全一致。直接按向量相似度排序它们未必能聚到一起。我在实体归一化层做了处理使用一个轻量NER模型抽取实体再用一个别名表把同实体的不同称呼映射成统一ID。这个ID会作为元数据写进Vector Engine查询时可以直接按实体ID过滤。这样搜索某公司股价波动时不会漏掉那些只出现了股票代码的报道。6.3 向量删除与更新的坑Vector Engine支持按ID删除和覆盖更新但有一个容易被忽略的细节索引更新不是实时立即可见。HNSW索引在删除和更新后有一段内部重组的时间。如果业务对强一致有要求测试时会发现删掉的数据偶尔还能被搜到。解决方法是控制写入频率和删除频率不要高频反复更新同一条数据。如果确实需要即时可见可以在应用层维护一个黑名单ID集合检索结果出来后做一层过滤。这是最终兜底的做法简单可靠。6.4 长上下文答案的截断与合成情报问答有时候会要求梳理近半年某方向的发展脉络检索结果可能是20个片段全塞给模型会超出上下文限制。我的做法是分批处理把20个片段按主题相关性聚类先让模型对每个聚类分别生成小段小结再把所有小结合并成一份完整报告。这个分治生成的策略效果很好。模型在一次性处理少量文本时注意力更集中生成内容的结构也清晰很多。如果直接在超大上下文里生成经常出现中间部分信息被忽略的情况。7. 架构的可扩展性从单一文本情报到多模态7.1 横向扩展的基础整套系统能扩展到多模态核心原因是Vector Engine只关心向量文本向量和图片向量在存储层没有任何区别。视频如果抽了关键帧把关键帧再过一遍图像向量模型一样能入库。音频可以先用语音转写变成文本再走文本链路。这个设计让我的系统在面对不同数据源时只需要新接入相应的理解模块后端的存储和检索链路完全不用动。具备多模态能力后情报系统才算真正完整。某一个突发事件用户只给你一张现场照片你可以通过图文联合检索找到相关的历史报道、时间线、背景资料这种能力在纯文本时代完全无法实现。7.2 接入大模型的能力边界在应用层接入了大模型之后我发现最终系统的能力上限并不仅仅由模型决定而是由你给它喂的上下文质量决定的。最理智的用法是把确定性的语义检索交给Vector Engine把可验证的事实信息存到存储层让模型只做归纳、对比、推演这类需要语言能力的工作。这样搭出来的系统非常稳即使未来换更强的模型也只需要改Prompt和驱动逻辑底层的情报积累不会废掉。最后想说的话重构这套系统的过程中我最大的体会是不要迷信某个单独的组件能解决所有问题。Vector Engine解决的是高强度语义召回大模型解决的是理解和生成它们各司其职组合起来才是一个真正好用的系统。另外如果从零开始搭千万不要一上来就追求大而全。先跑通一条最小链路——单个数据源采集、切块向量化、TopK检索、大模型回答——然后在这个基础上一块一块往上加每次只改一个变量出了问题你才知道去哪儿找原因。这条路虽然慢但每一步都在可控范围之内最终出来的系统也远比一次性堆叠出来稳定得多。
阅读完成 · 觉得有帮助?