1. 项目概述:一个全新的检索引擎架构
最近在折腾一个挺有意思的开源项目,叫dzhng/deep-seek。注意,这可不是那个同名的AI模型,而是一个实验性的、基于大语言模型(LLM)构建的互联网规模检索引擎架构。这个概念和我们平时接触的“研究型智能体”有本质区别,后者更像是一个“答案引擎”。
简单来说,我们熟悉的Perplexity、GPT Researcher这类工具,目标是聚合大量信息,为你找到一个“最正确”的答案,最终产出一份研究报告。而Deep-Seek这个项目,目标完全不同:它要处理海量信息源,目的是收集一份尽可能全面的实体列表。它的最终产出不是一段总结性文字,而是一个结构化的表格,里面罗列了所有检索到的实体,并且为每个实体补充了丰富的属性列。你可以把它想象成一个由AI驱动的、能够自动从全网抓取并结构化特定领域信息的超级爬虫+数据分析师。
我花了不少时间研究它的代码和设计思路,发现它采用了一种称为“流程工程”的多步骤智能体架构。整个系统就像一个精密的流水线,将用户的一个模糊查询,拆解成计划、搜索、提取、丰富四个核心阶段,最终构建出那个庞大的知识表格。这种设计思路对于处理复杂、开放域的检索任务非常有启发性,尤其是当你需要的不只是一个答案,而是一张“地图”时。
2. 核心架构与设计哲学解析
2.1 答案引擎 vs. 检索引擎:根本性的范式转变
在深入技术细节前,我们必须先厘清这个项目的核心设计哲学,因为它决定了后续所有的技术选型和实现路径。
传统答案引擎的思维是收敛式的。你问“2023年最好的开源向量数据库有哪些?”,它会去阅读几十篇博客、评测文章、官方文档,然后综合这些信息,给你一个它认为最靠谱的排名或总结,比如“Pinecone, Weaviate, Qdrant...”。它的目标是终结讨论,给出一个权威结论。这个过程高度依赖于LLM的总结和推理能力,但不可避免地会丢失大量原始信息和长尾数据。那些没进入“前三名”但可能有特殊优势的项目,或者那些新兴的、讨论度还不高的项目,很可能就被过滤掉了。
Deep-Seek的检索引擎思维则是发散式的。面对同样的问题,它的首要目标不是下结论,而是尽可能全地发现。它会试图找出所有在互联网上被提及的、可能与“开源向量数据库”相关的实体(项目、产品、工具),然后为每个实体去收集各种属性:创建时间、主要编程语言、GitHub星数、核心特点、近期更新时间等等。最终给你一张可能有几十行甚至上百行数据的表格。这张表格本身不直接告诉你“哪个最好”,但它为你提供了进行深度分析和比较的完整原材料。它的目标是开启探索,将信息的广度和结构交还给用户。
这种范式转变带来了独特的技术挑战:如何高效地从非结构化网页中批量识别和提取实体?如何为海量实体并行地收集和验证属性信息?如何评估并呈现每条信息的置信度?这些都是Deep-Seek架构要解决的核心问题。
2.2 四阶段流水线:从模糊查询到结构化表格
项目的架构清晰地划分为四个顺序执行的阶段,形成了一个严谨的“流程工程”管道。每个阶段都承担着特定的职责,并将产出传递给下一阶段。
第一阶段:计划这是整个流程的“大脑”。接收原始用户查询(例如:“找出专注于可持续时尚的欧洲初创公司”)后,计划器智能体(由Claude等LLM驱动)会进行深度解析。它的核心任务是定义输出结果的“形状”,具体包括:
- 目标实体类型:明确我们要找的是什么。是“公司”、“产品”、“学术论文”、“人物”还是“事件”?在上例中,它被定义为“初创公司”。
- 表格列定义:明确我们需要为每个实体收集哪些信息。这直接来自对查询意图的推理。对于“可持续时尚的欧洲初创公司”,计划器可能会定义出以下列:
公司名称、所在地(国家/城市)、成立年份、核心产品/服务、可持续性实践描述、融资阶段、官方网站。 这个计划的输出是一个结构化的模式(Schema),它指导后续所有步骤的进行,确保搜索和提取的目标明确。
第二阶段:搜索有了明确的实体类型和相关的上下文列,搜索阶段的任务就是为“提取”阶段准备原材料。项目使用了 Exa.ai 的搜索API,并巧妙地混合了两种搜索策略:
- 关键词搜索:擅长发现用户生成内容。例如,搜索“sustainable fashion european startup review”或“top eco-friendly clothing brands in Berlin list”。这类文章、博客、论坛帖子通常包含对多个实体的描述、比较和评价,是发现实体的富矿。
- 神经搜索:擅长进行语义匹配,直接找到实体本身。例如,直接搜索“European sustainable fashion startup”或“companies making clothes from recycled materials”。这种搜索能更直接地定位到公司官网、Crunchbase页面等权威信息源。 这种混合策略确保了信息源的多样性和覆盖率,既抓取了深度讨论,也锁定了具体目标。
第三阶段:提取这是将非结构化文本转化为结构化数据的核心环节。系统需要从第二阶段获取的网页内容中,精准地“抠出”计划阶段所定义的实体。项目采用了一种新颖且高效的方法:
- 内容分块:首先使用轻量级的
winkNLP模型将大段文本按句子进行分割。 - 标记插入:在每个句子之间插入一个特殊的、唯一的标记符(可以想象成给每个句子边界打上一个ID)。
- LLM范围标注:然后将带有标记的文本发送给LLM。LLM的任务不是重写或总结,而是极其精确地指出:哪些连续的标记(即哪些句子)共同描述了一个符合要求的实体。例如,它可能输出
[start: token_45, end: token_52],表示从第45个标记到第52个标记之间的句子描述了一家名为“Reform”的初创公司。 这种方法的最大优势是极高的令牌(Token)使用效率。LLM无需重复输出整个实体描述,只需输出标记的范围索引,极大地降低了处理长文档的成本和耗时,使得大规模提取成为可能。
第四阶段:丰富提取阶段我们得到了一个实体名称的列表。丰富阶段的任务就是为每个实体填充计划中定义的所有属性列。这实际上是在流水线内部嵌入了一个个微型的“答案引擎”。 系统会为每个实体、每个待填充的列,发起一次聚焦的查询。例如,对于实体“Reform”,为了填充“融资阶段”这一列,它可能会构造搜索:“Reform sustainable fashion startup funding series”。然后从搜索结果中提取或总结出相关信息。这是整个流程中最耗时、计算成本最高的部分,因为实体和列的数量乘积可能非常大。但正是这一步,赋予了最终表格巨大的价值,将简单的名称列表变成了一个信息丰富的知识图谱片段。
3. 技术实现与实操要点
3.1 环境搭建与项目运行
项目基于Next.js框架构建,前端展示和后端逻辑集成在一起。要跑起来,你需要准备好Node.js环境和必要的API密钥。
第一步:获取API密钥这是产生实际成本的关键。项目依赖两个核心服务:
- Anthropic Claude API:作为整个流程中多个智能体(计划器、提取器、丰富器)的“大脑”。你需要去Anthropic官网注册并获取API密钥。Claude模型(如Claude 3 Sonnet/Haiku)在遵循复杂指令和结构化输出方面表现优异,非常适合此类多步骤代理任务。
- Exa.ai API:提供高质量的混合搜索(关键词+神经)功能。你需要去Exa官网注册获取密钥。Exa的搜索质量直接决定了能找到多少相关且高质量的原始材料,是整个流程的“原料入口”。
第二步:配置与启动克隆项目后,在根目录创建.env.local文件,填入你的密钥:
ANTHROPIC_API_KEY="你的_claude_api_密钥" EXA_API_KEY="你的_exa_api_密钥"然后使用你喜欢的包管理器安装依赖并启动开发服务器:
pnpm install # 推荐使用pnpm,速度更快 pnpm dev访问http://localhost:3000即可打开界面。项目内置了几个示例查询(如“AI video generation startups”),你可以直接查看这些查询的静态结果。这些结果是预先跑好、存储在项目里的,目的是让你直观感受最终表格的规模和形态,而无需花费API credits。
第三步:运行自定义查询如果你想亲自体验完整的流水线,在界面输入框输入你的问题,然后点击搜索。这里有几个非常重要的注意事项:
注意:运行一次真实查询的成本和耗时不容小觑。根据我的测试,一个中等复杂度的查询(如检索20-30个实体),可能需要3-8分钟,消耗的API费用在0.5到2美元之间。费用主要取决于:1)搜索返回的网页数量;2)提取出的实体数量;3)每个实体需要丰富的属性列数量。启动前请务必知晓。
启动查询后,务必打开你的终端。开发者将详细的日志输出到了控制台,你可以实时看到流水线的运行状态:
[Planner] 正在分析查询:“sustainable fashion startups in Europe”... [Planner] 计划生成完毕。目标实体:`startup`。定义列:[name, location, founding_year, core_product, sustainability_practice]。 [Searcher] 开始混合搜索。关键词搜索词:“sustainable fashion europe startup review”。神经搜索词:“European eco clothing company”。 [Searcher] 从Exa获取了42个潜在相关内容URL。 [Extractor] 正在处理URL-1 (https://example.com/blog-top-10-eco-brands)... 插入256个句子标记。 [Extractor] LLM识别出3个实体片段:[token_12-18], [token_45-55], [token_102-110]。 [Enricher] 开始丰富实体 #1 “Reform”。查询“Reform startup location”... [Enricher] 实体 #1 “Reform” - 列`location` 已填充为 “Berlin, Germany”。置信度:0.92。观察这些日志是理解系统工作原理的最佳方式。
3.2 核心代码模块剖析
项目的代码结构清晰地反映了其四阶段架构,主要逻辑位于app/api/search/route.ts这个Next.js API路由中。
计划器模块计划器通常是一个对Claude API的调用,使用精心设计的系统提示词(System Prompt)来约束输出格式。提示词会要求LLM以严格的JSON格式输出,包含entity_type和columns两个字段。columns本身是一个数组,每个元素定义列名和简短描述。这是整个流程的“宪法”,后续所有步骤都必须遵守。
搜索器模块搜索器封装了对Exa API的调用。关键点在于它如何根据计划生成搜索词。对于“关键词搜索”,它可能会将查询与“review”、“list”、“2024”、“best”等词汇组合,以寻找列表类文章。对于“神经搜索”,它可能会直接使用“{entity_type} about {query_topic}”这样的句式。项目可能需要在这里进行一些启发式规则或二次LLM调用,以优化搜索词生成策略。
提取器模块这是技术实现上最精巧的部分。伪代码逻辑如下:
async function extractEntities(content, plan) { // 1. 使用winkNLP进行句子分割 const sentences = winkNLP(content).sentences().out(); // 2. 插入特殊标记,例如用`<s_0>`, `<s_1>`... 包裹每个句子 const taggedContent = sentences.map((sent, idx) => `<s_${idx}>${sent}</s_${idx}>`).join(' '); // 3. 构造给LLM的提示词,要求其识别描述实体的句子范围 const extractionPrompt = ` 你是一名信息提取专家。以下是带有标记的文本: ${taggedContent} 请找出所有关于【${plan.entity_type}】的提及。对于每一个提及,请提供包含完整描述的起始标记<s_i>和结束标记<s_j>。 输出格式为JSON数组:[{"start_tag": "<s_i>", "end_tag": "<s_j>"}, ...] `; // 4. 调用LLM并解析结果 const llmResponse = await callClaude(extractionPrompt); const ranges = JSON.parse(llmResponse); // 5. 根据标记范围,还原出原始句子,组合成实体描述文本 const entities = ranges.map(range => { const startIdx = parseInt(range.start_tag.match(/<s_(\d+)>/)[1]); const endIdx = parseInt(range.end_tag.match(/<s_(\d+)>/)[1]); return sentences.slice(startIdx, endIdx + 1).join(' '); }); return entities; // 返回提取出的实体描述文本数组 }这种方法避免了LLM重复输出长文本,极大提升了效率。
丰富器模块丰富器是一个循环处理过程。对于提取出的每个实体描述文本,以及计划中定义的每个列,它都会发起一次独立的“问答”调用。
for (const entityText of extractedEntities) { for (const column of plan.columns) { const enrichmentPrompt = ` 基于以下关于某个实体的上下文: ${entityText} 请回答:这个实体的【${column.name}】(${column.description})是什么? 如果上下文没有明确信息,请基于常识进行合理推断,并说明这是推断。 请以简洁的短语或句子回答。 `; const answer = await callClaude(enrichmentPrompt); // 同时,可以要求LLM为这个答案提供一个置信度评分(0-1) const confidence = await getConfidence(entityText, column, answer); tableRows.push({ entity: entityName, // 从entityText中解析出的名字 [column.name]: answer, [`${column.name}_confidence`]: confidence }); } }这个过程是并发的理想目标,但实际中可能因为API速率限制需要做队列管理。
3.3 结果呈现与置信度机制
前端界面以表格形式展示最终结果,这是项目的一大亮点。除了展示数据,它还通过颜色编码(如黄色高亮)直观地显示了每个单元格数据的置信度。
置信度评分(0-1)是在丰富阶段由LLM自行评估产生的。系统可能会要求LLM在给出答案的同时,基于所参考的源文本片段,评估该答案的可靠程度。低置信度可能源于:
- 信息冲突:不同来源对同一事实的描述不一致。
- 信息缺失:源文本中没有直接提及,LLM进行了推测。
- 表述模糊:源文本中的描述本身就不清晰。
这个功能极具价值,它诚实地向用户揭示了AI生成内容的不确定性,将判断权部分交还给用户。在实际使用中,对于高置信度的数据可以相对信任,对于低置信度(例如<0.7)的单元格,用户就需要保持警惕,将其作为进一步手动核查的线索。
4. 潜在问题、优化方向与实践心得
4.1 常见挑战与排查思路
在实际运行和借鉴该架构时,你可能会遇到以下典型问题:
1. 成本失控这是最实际的问题。一次查询轻松花费数美元,对于频繁使用是不可接受的。
- 排查与优化:
- 检查日志:首先通过终端日志,看是哪个阶段消耗了大部分Token。通常是“丰富”阶段,因为它需要为(实体数量 × 列数量)个组合生成查询和答案。
- 优化计划器:调整计划器的提示词,要求它定义更少、更关键的列。不必要的列会指数级增加成本。
- 设置预算与限制:在代码层面硬性限制搜索返回的最大网页数(如20个)、提取的最大实体数(如30个)。可以在API调用循环中加入成本累计器,达到阈值立即停止。
- 使用更小/更便宜的模型:在提取和丰富阶段,尝试使用更便宜的模型,如Claude Haiku或GPT-3.5-Turbo,虽然效果可能略有下降,但成本大幅降低。
2. 运行时间过长查询需要几分钟甚至更久,体验不流畅。
- 排查与优化:
- 并发与速率限制:检查代码是否完全串行执行。丰富阶段对不同实体、不同列的查询是天然可并行的。使用
Promise.all或队列(如p-queue)进行并发控制,同时注意遵守Anthropic和Exa的速率限制。 - 缓存策略:对于相同的搜索词或相似的实体查询,可以引入缓存层(如Redis)。例如,对“OpenAI founding year”的查询结果可以被缓存,下次遇到时直接使用。
- 简化提取逻辑:评估提取阶段是否过度复杂。有时简单的正则表达式或基于规则的匹配(针对特定实体类型)可能比LLM调用更快、更便宜。
- 并发与速率限制:检查代码是否完全串行执行。丰富阶段对不同实体、不同列的查询是天然可并行的。使用
3. 结果质量不佳(重复、错误、不相关)
- 重复实体问题:系统可能将“MacBook Pro M2”和“Apple MacBook Pro with M2 chip”识别为两个不同实体。
- 优化思路:在提取后、丰富前,增加一个“实体解析”步骤。使用文本相似度算法(如TF-IDF向量化后计算余弦相似度)或轻量级LLM调用,对提取的实体名称进行聚类和去重。
- 信息错误问题:丰富阶段LLM可能“幻觉”出错误信息。
- 优化思路:强化丰富阶段的提示词,要求“严格基于提供的上下文,不要自行编造”。同时,实现一个简单的“溯源”功能,让每个单元格的答案都能关联回被引用的原始句子或URL,方便用户追溯核查。
- 实体不相关:搜索阶段带回了大量噪音。
- 优化思路:优化搜索词生成策略。可以先用一个小型LLM调用对用户查询进行改写,生成多个不同侧重点的搜索查询。同时,利用Exa API的过滤参数(如时间范围、网站域名等)进行初步筛选。
4.2 架构扩展与未来演进思考
原项目作者也提出了一些未来的方向,结合我的经验,我认为以下几个扩展点非常有价值:
1. 结果排序与排名目前的表格只是简单罗列。对于“最好的XX”、“最新的XX”这类查询,增加一个排序层至关重要。这可以在丰富阶段之后,引入一个专门的“评分智能体”。该智能体基于所有已收集到的列数据,对实体进行综合评分或排序。例如,对于“最佳开源模型”,评分可以基于GitHub Stars增长趋势、近期Commit活跃度、社区讨论热度(来自搜索结果的引用频率)等维度进行加权计算。
2. 深度浏览与交互式检索对于某些深度内容(如学术论文PDF、需要登录的页面、多级菜单的网站),简单的抓取摘要是不够的。需要引入一个能够模拟点击、滚动、填写表单的“浏览器智能体”(例如结合Playwright或Puppeteer)。这会使系统能力更强,但复杂度和成本也急剧上升,更适合作为特定垂直领域的增强功能。
3. 流式结果输出当前用户体验是“黑盒”等待几分钟,然后一次性获得所有结果。实现流式输出(SSE或WebSocket)可以极大改善体验。架构可以调整为:每提取出一个实体,就立即将其名称流式推到前端;每丰富完一个实体的一个属性,就更新前端的表格单元格。这让用户能实时感知进度,并对先出来的结果进行初步分析。
4. 从表格到知识图谱目前的输出是扁平的表格。一个自然的演进是将每次查询的结果存储下来,并逐步构建一个互联的知识图谱。例如,本次查询了“AI视频生成初创公司”,下次查询了“这些公司的投资方”,系统可以将两次结果关联起来,形成“初创公司-被投资于-投资机构”的关系边。长期积累,可以形成一个由AI自主扩展的领域知识库。
4.3 个人实践心得与建议
经过一段时间的摸索,我有几点深刻的体会:
关于使用场景:Deep-Seek这种架构并不适合替代谷歌搜索或Perplexity来解决你日常的“快速问答”需求。它的强项在于竞争分析、市场调研、学术文献综述、长尾信息收集等需要“广撒网”的场景。比如,你想了解“全球正在研究钙钛矿太阳能电池的实验室”,用它来生成一个包含实验室名称、所在地、主要研究方向、近期论文标题的表格,效率远超人工。
关于提示词工程:这个系统的表现极度依赖于给各个阶段LLM的提示词。计划器的提示词决定了方向的正确性;提取器的提示词决定了召回率和准确率;丰富器的提示词决定了数据质量。你需要像调试程序一样,用大量不同的查询用例去测试和迭代这些提示词,找到最稳定、最可靠的表述方式。
关于成本控制:在投入生产环境前,必须建立严格的成本监控和熔断机制。可以为不同复杂度的查询预设不同的“预算档位”。简单查询限制搜索5个网页、最多提取10个实体;复杂查询则放宽限制。同时,一定要记录每次查询的详细成本日志,分析性价比,持续优化。
关于评估体系:如何评估这样一个系统的输出好坏?准确率、召回率等传统指标计算起来非常困难,因为缺乏“标准答案”。一个实用的方法是进行“抽样人工评估”:定期随机抽取一些查询结果,人工检查实体是否相关、属性列是否准确。同时,可以设计一些“可验证”的测试查询(如查询“2023年诺贝尔奖获得者”,其信息是明确且可核对的),来监控系统的性能波动。
这个项目为我们展示了一种超越简单问答的、更具结构性和探索性的AI应用范式。它把LLM从“答题者”变成了“信息架构师”和“数据矿工”。虽然目前仍处于实验阶段,在成本、速度和准确性上还有很长的路要走,但其设计思想无疑为下一代AI驱动的信息检索工具指明了方向。如果你正在构建需要深度信息聚合的产品,深入研究这个架构的每一个环节,将会带来非常多的启发。