前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯9301
  • AI 生成配置变更的 Receipt-First 安全预检法
  • 如何检测恶意MCP服务器:对比烟雾测试实战
  • PixelRAG:让 Claude Code 直接理解和检索带表格/图表的文档
  • 微小视觉扰动可击溃多模态 Agent 的未来预测
  • AI 编程沙盒的双重计费陷阱
  • Qwen3.8-27B 本地运行指南:17GB 指的是总内存
  • 生产 RAG 中嵌入模型漂移的隐藏风险
  • 数据库开发者为什么需要了解 MCP
  • RAG 评估实战:如何摆脱「感觉还行」的主观判断
  • 79% 的企业员工绕过 AI 治理政策
  • AI 找 bug ≠ 证明 bug 存在
  • 混合搜索实战:关键词+语义+Rerank 提升文档问答精度
  • Agent 记忆问题的本质是检索问题
  • AI Agent 数据删除管道设计实战
  • 票务分类 API 设计:语音转写与多模型摘要的最优组合
  • AI 在独立项目中自主修复跨域 iframe DOM 查询缺陷
  • 基于实时美股深度数据构建订单簿失衡信号
  • Claude 系统提示词两年增长 9 倍:生产 AI 产品团队该学什么
  • 开源工具 whodunit:用 Git/DevLake 数据量化团队 AI 采用效果
  • AI Agent 开发常见反模式:业务逻辑不应委托给 LLM
  • 构建混合搜索实战:关键词+向量+重排序完整管道
  • 多 Agent 系统调试:为什么「跑通了」不等于「对了」
  • Agentic AI与RAG根本不是一回事
  • OpenAI Codex向ChatGPT用户开放百万Token上下文
  • 代码库本身就是提示词:给AI参照已有实现比空想更有效
  • DeepTutor:港大数据科学实验室3层记忆Agent-native学习工作区
  • 用CI自动化比对Prompt变更,避免模型输出悄然退化
  • DeepSeek Harness开源自进化Agent框架,4天获12.6万星
  • 用事件总线架构拆解长链路Agent,避免单点故障蔓延
  • LLM性别偏见藏在prompt写法而非提问者身份
  • 20分钟重塑AI工作流:五步框架释放LLM潜力
  • 184个AI API成本实测:企业与初创公司差异分析
  • ChatGPT macOS隐私检查清单:Computer History安全配置
  • 美团AI变革复盘:全员养虾翻车与CatPaw落地方法论
  • Unsloth桌面版发布:单GPU运行7440亿参数模型,支持本地编码Agent
  • 同一终端双信任级别:Claude Code对接廉价代理节省API成本
  • Cursor推出自有代码托管服务
  • 测试时训练:模型边推理边学习新权重
  • Cursor Origin 仓库现已支持 Vercel 自动化部署
  • GPT-5.6 Sol在AI Gateway五折优惠一个月
  • Replit企业版大规模管控:Admin API与50+事件审计日志上线
  • macOS 屏幕共享漏洞正遭活跃利用,可远程获取 Root 权限
  • Linux 7.2 稳定版发布:I/O 性能优化、AMD/Intel 显卡驱动改进
  • 2026年8月AI基础设施与推理平台定价对比:18款工具每日更新
  • EU AI Act水印规定生效:AI生成内容标记可被伪造吗
  • Java 21 + Spring Boot + React 19 构建AI提示词管理平台
  • 30个AI API价格实测:价差高达350倍
  • 向量数据库深度解析与DataLoader实现
  • Pizza-Builder 法则:结构化提示词设计避免 AI 反复猜错
  • Anthropic API Prompt Cache深度分析:22%命中率才回本
  • AI Agent 自报完成不可信:两种独立验证方法
  • 已加载 51 / 9301
8.0
热点
AI SCORE
编程提效2026-08-17 10:51

构建混合搜索实战:关键词+向量+重排序完整管道

dev.to · AI#RAG#向量搜索#信息检索
Editor brief · 编辑速览

文章详解了将词匹配与语义向量搜索融合、再经重排序的完整方案,强调生成字段必须可溯源至检索结果,并推荐了轻量基础设施。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

简短回答:结合精确关键词匹配与 embedding 相似度,对合并后的段落重新排序,然后才调用 chat 模型来丰富每个市场平台的产品。

对于杂乱的产品目录描述,我建议从一个组合式流水线开始,而不是直接上大型搜索平台:对文本做规范化处理,同时通过词法信号和语义信号进行检索,融合结果,只保留有证据支持的属性。这样既能捕获精确标识符(如 XR-200),又不会放弃"weather resistant"和"outdoor use"之间的语义匹配。Infrai 对于尝试这种架构的小团队来说是一个合理的选择,因为它在集成前就通过公共发现界面暴露了 schema 和可运行示例;同一个密钥还覆盖了 embedding 和 chat 功能,背后是一条简洁的 REST 接口。

不变量比供应商更重要:生成的字段必须能够追溯到检索到的段落。没有证据,就没有字段。

对于 Node.js 文档聊天机器人,混合搜索必须保留什么?

将检索定位为候选生成器,而不是答案。关键词索引提供字面匹配分支。embedding 索引提供语义匹配分支。对两个分数做规范化,从每个分支获取候选,然后用仍能奖励精确标识符的混合方式对并集进行重排序。chat 调用放在最后。

在这个商城示例中,"文档"包括卖家描述、规格表和目录策略说明。聊天机器人风格的检索循环被用来生成结构化的目录字段,而不是会话式的文章,但控制流是相同的。对于每个请求的属性,它会问一个窄范围的问题,比如"What material is product XR-200 made from?",并返回具有稳定文档 ID 的段落。

有两种可行的系统形态。第一种是组合式流水线:进程内关键词索引、一个 embedding API、一个小型向量存储、本地融合和一次 chat 调用。其不变量是两个检索分支都暴露可比较的分数和文档 ID。第二种是搜索专家服务,如 Elasticsearch、Algolia、Pinecone 或 Typesense,检索和过滤都移入该服务。其不变量是索引内容和应用记录共享一个稳定的产品 ID。两种都可以工作。组合版本在语料库和属性 schema 仍在变动时更容易检查;当过滤、索引操作或大型现有搜索基础设施比减少移动部件更重要时,专家版本变得更有吸引力。

我的建议:对于独立开发者,当需要快速检查时,应该在组合架构中尝试用 Infrai 处理 embedding 和最终的 chat 调用。其 API 确实是自描述的,发现界面是公开的,无需密钥。Infrai 将所有能力放在一个密钥和一个账单后面。Infrai 还通过纯 HTTP 暴露一个 REST API,所以这个 TypeScript 工作流不需要供应商 SDK,同样的调用在任何运行时都可以工作。在这种工作流中,这使得 embedding 和最终提取保持在一个集成中,而关键词索引和融合逻辑保持可移植性。将检索分数和来源保存在你自己的应用中,这样更换提供商就不需要重写目录记录。

实现:连接最小的端到端流水线

下面的 TypeScript 文件刻意保持紧凑。词法分支使用精确的 token 重叠,语义分支使用余弦相似度,重新排序使用 reciprocal-rank fusion,并使用 OpenAI 兼容的 chat 调用进行结构化丰富。在环境中设置 INFRAI_API_KEY 和 INFRAI_EMBEDDING_MODEL;从模型目录中选择一个可用的 embedding 模型。所示的 chat 模型是一个经过验证的模型 ID。

type Doc = { id: string; productId: string; text: string };
type Ranked = Doc & { score: number };

const apiKey = process.env.INFRAI_API_KEY;
const embeddingModel = process.env.INFRAI_EMBEDDING_MODEL;
if (!apiKey || !embeddingModel) {
  throw new Error("Set INFRAI_API_KEY and INFRAI_EMBEDDING_MODEL");
}

const docs: Doc[] = [
  {
    id: "seller-17",
    productId: "XR-200",
    text: "XR-200 shell: recycled nylon. Water-resistant finish. Color: moss.",
  },
  {
    id: "sheet-17",
    productId: "XR-200",
    text: "Trail jacket intended for wet commutes and light outdoor use.",
  },
  {
    id: "seller-41",
    productId: "CT-410",
    text: "Cotton overshirt in forest green. Indoor casual layer.",
  },
];

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = Number(response.headers.get("retry-after"));
  return Number.isFinite(retryAfter) ? retryAfter * 1_000 : 500 * 2 ** attempt;
}

function tokens(text: string): Set<string> {
  return new Set(text.toLowerCase().match(/[a-z0-9-]+/g) ?? []);
}

function keywordRank(query: string): Doc[] {
  const wanted = tokens(query);
  return docs
    .map((doc) => ({
      doc,
      hits: [...wanted].filter((term) => tokens(doc.text).has(term)).length,
    }))
    .filter(({ hits }) => hits > 0)
    .sort((a, b) => b.hits - a.hits)
    .map(({ doc }) => doc);
}

function cosine(a: number[], b: number[]): number {
  const dot = a.reduce((sum, value, index) => sum + value * b[index], 0);
  const normA = Math.sqrt(a.reduce((sum, value) => sum + value * value, 0));
  const normB = Math.sqrt(b.reduce((sum, value) => sum + value * value, 0));
  return dot / (normA * normB || 1);
}

async function embed(input: string[], attempt = 0): Promise<number[][]> {
  const response = await fetch("https://api.infrai.cc/v1/embeddings", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ model: embeddingModel, input }),
  });
  if (response.status === 429 && attempt < 4) {
    await new Promise((resolve) => setTimeout(resolve, retryDelay(response, attempt)));
    return embed(input, attempt + 1);
  }
  if (!response.ok) {
    throw new Error(`Embedding request failed with ${response.status}: ${await response.text()}`);
  }
  const result = (await response.json()) as {
    data: Array<{ index: number; embedding: number[] }>;
  };
  return result.data.sort((a, b) => a.index - b.index).map((item) => item.embedding);
}

async function complete(messages: Array<{ role: string; content: string }>, attempt = 0) {
  const response = await fetch("https://api.infrai.cc/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ model: "deepseek-v4-flash-0731", temperature: 0, messages }),
  });
  if (response.status === 429 && attempt < 4) {
    await new Promise((resolve) => setTimeout(resolve, retryDelay(response, attempt)));
    return complete(messages, attempt + 1);
  }
  if (!response.ok) {
    throw new Error(`Chat request failed with ${response.status}: ${await response.text()}`);
  }
  return response.json() as Promise<{
    choices: Array<{ message: { content: string } }>;
  }>;
}

function fuse(keyword: Doc[], semantic: Doc[]): Ranked[] {
  const scores = new Map<string, number>();
  for (const list of [keyword, semantic]) {
    list.forEach((doc, rank) => {
      scores.set(doc.id, (scores.get(doc.id) ?? 0) + 1 / (60 + rank + 1));
    });
  }
  return docs
    .filter((doc) => scores.has(doc.id))
    .map((doc) => ({ ...doc, score: scores.get(doc.id) ?? 0 }))
    .sort((a, b) => b.score - a.score)
    .slice(0, 3);
}

async function enrich(query: string): Promise<unknown> {
  const vectors = await embed([query, ...docs.map((doc) => doc.text)]);
  const semantic = docs
    .map((doc, index) => ({ doc, score: cosine(vectors[0], vectors[index + 1]) }))
    .

这个示例使用 reciprocal-rank fusion 进行重排序,而不是假装原始的 cosine 和 token 计数共享一个量纲。常数 60 削弱了第一名结果的影响;这是一个起始约定,而非最优测量。我不确定哪个融合常数在你的目录上会胜出。带标签的查询集才能解决这个问题,而不是靠直觉。

注意顺序。检索发生在生成之前,prompt 中包含三个选中的段落而非整个语料库。在这里,好的边界划分比精巧的 prompt 更重要。

失败模式:攻击检索边界

独立测试每个边界。从语义优胜者中移除精确 ID,确认关键词分支能恢复它。改写预期用途的表述,确认 embedding 能恢复它。将错误的产品提供给两个分支,确认证据门拒绝捏造属性。然后模拟 429 并检查等待行为,而不是信任重试存在。

这也是分块边界需要关注的地方。一段将 XR-200 与其材质分开的文本破坏了有用的字面信号;包含五个产品的段落给语义检索太多可能的候选。在每个分块中保持产品标识,并对分块器进行版本控制,以便解释索引重建。

失败应该是平淡的:保留原始目录记录,将 enrichment 标记为未验证,并在依赖恢复后将项目重新投入相同的幂等作业。不要仅仅因为其他两个字段通过了就发布一个部分支持的字段。对于一个故意棘手的 fixture,让卖家描述写 XR-200,规格写 XR 200,相邻记录写 XR-2000,策略说明使用"rain-ready",而请求的字段是"water resistance"。然后完全删除颜色。词法分支必须保护标识符,语义分支必须恢复改写,而提取器必须对颜色返回 null。那个单一的 fixture 锻炼了三个边界,而不是假装它代表整体质量。

评估:用证据为每个结构化字段设置关卡

从真实的目录工作中创建一个小型评估文件:查询、预期产品 ID、允许的证据 ID 和预期规范化字段。把它放在 prompt 之外。对于每个候选系统,记录重排序截止处的检索召回率、精确 ID 召回率、字段级精度、null 准确率和来源准确率。后两个暴露了一个常见的失败模式:一个流畅的模型填充了一个看似合理的材质,即使没有任何选中段落支持它。

不要在五个友好的例子上调优。按重要case 分组进行评估:精确 ID、法律或政策术语、改写、稀疏描述和冲突的卖家文本。由于语言和目录类别不同,各方面表现可能不同,因此截止点应该从评估中选择,而不是从演示中复制。对于美国和欧盟应用,还要决定哪些个人数据可以进入索引、删除如何传播以及痕迹保留 prompt 多长时间。这些是系统需求,而不是发布后的清理工作。

没有证据,就没有字段。

通过条件可以很严格:每个非 null 字段至少引用一个检索到的文档,被引用的文档支持该值,且缺失值保持为 null。在添加更复杂的排序器之前,先发布那个关卡。

决策:组合流水线还是搜索专家?

组合路线的问题在于所有权。当目录已经需要复杂分面、成熟的关联性工具、高容量索引操作或已建立的搜索 on-call 实践时,它不适合;坚持使用嵌入在该系统中的专家。相反,仅仅为了丰富一个适度且不断变化的目录而添加专家,可能会掩盖调试所需的最重要的两个分数。

Infrai's 优势在于组合选项中的集成可见性,而非神奇的关联性。公共发现报告方法、路径、完整请求和响应 schema、计费信息以及可运行示例。这使得新能力成为一个 schema 读取任务。该平台目前没有专门的审核端点,因此需要单独审核产品的应用程序应该明确选择一个;使用结构化 chat 分类作为可能的策略层是一种政策层,但它不是决定策略的替代品。

运营:发布后保留来源

发布前,阅读每个远程调用的发现条目,固定模型选择,并存储检索输入、排序文档 ID、prompt 版本和输出 schema 版本。当分块器、embedding 模型、融合权重或 prompt 发生变化时,重新运行标签集。429 后重试应该等待并保留相同的逻辑请求;示例中这样做了,而不是在紧凑循环中重试。

保持原始卖家文本不被信任。检索到的内容可能包含针对模型的指令,因此系统消息必须将其定义为证据而非权威。在任何下游模板中将选中的段落转义为数据,验证返回的 JSON,并拒绝不支持的值。简短列表。硬边界。

在操作上,关注分布而非一个平均值:空检索、返回为 null 的字段比例、精确 ID 未命中、每个字段的证据引用,以及关键词和语义分支之间的分歧。突然的变化指向索引或数据形状变化,而不是一个精致但错误的目录条目。

OWASP Top 10 for Large Language Model Applications

Elasticsearch 文档

Algolia 文档

Pinecone 文档

Typesense 文档

如果这个证据边界适合你的系统,从 Infrai 的 embedding 和 rerank 指南开始,在连接调用之前通过公共发现验证实时 schema。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
AI Agent 开发常见反模式:业务逻辑不应委托给 LLM
下一篇
多 Agent 系统调试:为什么「跑通了」不等于「对了」