[{"data":1,"prerenderedAt":793},["ShallowReactive",2],{"note:\u002Frag\u002F07_rag_在线链路":3},{"id":4,"title":5,"body":6,"date":772,"description":773,"draft":774,"extension":775,"featured":774,"kind":776,"lastmod":772,"meta":777,"navigation":77,"path":781,"planned":782,"section":783,"seo":784,"source":785,"stem":786,"tags":787,"toc":77,"weight":236,"__hash__":792},"notes\u002Frag\u002F07_RAG_在线链路.md","RAG 在线链路",{"type":7,"value":8,"toc":765},"minimark",[9,14,18,49,91,95,103,344,347,355,358,381,385,388,440,442,448,454,458,465,726,728,734,736,761],[10,11,13],"h2",{"id":12},"在线链路实战混合检索reranker-精排与流式生成","在线链路实战：混合检索、Reranker 精排与流式生成",[10,15,17],{"id":16},"_0-运行前准备","0. 运行前准备",[19,20,21,36,42],"ul",{},[22,23,24,25,30,31,35],"li",{},"先运行 ",[26,27,29],"a",{"href":28},"\u002Frag\u002F06_rag_%E7%A6%BB%E7%BA%BF%E9%93%BE%E8%B7%AF\u002F","RAG基础_离线链路","，确保 ",[32,33,34],"code",{},"rag_docs"," Collection 已创建并加载",[22,37,38,39],{},"已在运行环境中配置 ",[32,40,41],{},"DASHSCOPE_API_KEY",[22,43,44,45,48],{},"如果在独立脚本中运行，请先补齐下面的初始化代码，后续示例会复用 ",[32,46,47],{},"ef"," 变量",[50,51,56],"pre",{"className":52,"code":53,"language":54,"meta":55,"style":55},"language-python shiki shiki-themes github-light github-dark","from pymilvus import Collection, connections\nfrom pymilvus.model.hybrid import BGEM3EmbeddingFunction\n\nconnections.connect(uri=\".\u002Frag_demo.db\")\nef = BGEM3EmbeddingFunction(use_fp16=False, device=\"cpu\")\n","python","",[32,57,58,66,72,79,85],{"__ignoreMap":55},[59,60,63],"span",{"class":61,"line":62},"line",1,[59,64,65],{},"from pymilvus import Collection, connections\n",[59,67,69],{"class":61,"line":68},2,[59,70,71],{},"from pymilvus.model.hybrid import BGEM3EmbeddingFunction\n",[59,73,75],{"class":61,"line":74},3,[59,76,78],{"emptyLinePlaceholder":77},true,"\n",[59,80,82],{"class":61,"line":81},4,[59,83,84],{},"connections.connect(uri=\".\u002Frag_demo.db\")\n",[59,86,88],{"class":61,"line":87},5,[59,89,90],{},"ef = BGEM3EmbeddingFunction(use_fp16=False, device=\"cpu\")\n",[10,92,94],{"id":93},"_1-dense-sparse-混合检索","1. Dense + Sparse 混合检索",[96,97,98,99,102],"p",{},"混合检索把两条检索路线并行执行：Dense 负责语义理解，Sparse 负责精确关键词匹配，再用 ",[32,100,101],{},"WeightedRanker"," 把两路结果融合成一份候选列表。",[50,104,106],{"className":52,"code":105,"language":54,"meta":55,"style":55},"from pymilvus import AnnSearchRequest, Collection, WeightedRanker\n\n\ndef hybrid_search(query: str, top_k: int = 20) -> list[dict]:\n    collection = Collection(\"rag_docs\")\n    query_embeddings = ef([query])\n\n    dense_req = AnnSearchRequest(\n        [query_embeddings[\"dense\"][0]],\n        \"dense_vector\",\n        {\"metric_type\": \"IP\", \"params\": {}},\n        limit=top_k,\n    )\n    sparse_req = AnnSearchRequest(\n        [query_embeddings[\"sparse\"][[0]]],\n        \"sparse_vector\",\n        {\"metric_type\": \"IP\", \"params\": {}},\n        limit=top_k,\n    )\n\n    results = collection.hybrid_search(\n        [sparse_req, dense_req],\n        rerank=WeightedRanker(0.7, 1.0),\n        limit=top_k,\n        output_fields=[\"text\", \"source\", \"parent_id\"],\n    )[0]\n\n    return [\n        {\n            \"id\": hit.id,\n            \"text\": hit.get(\"text\"),\n            \"source\": hit.get(\"source\"),\n            \"parent_id\": hit.get(\"parent_id\"),\n        }\n        for hit in results\n    ]\n\n\ncandidates = hybrid_search(\"新员工年假怎么计算？\")\nprint(f\"混合检索召回：{len(candidates)} 个候选片段\")\nfor item in candidates[:3]:\n    print(item[\"parent_id\"], item[\"text\"][:40])\n",[32,107,108,113,117,121,126,131,137,142,148,154,160,166,172,178,184,190,196,201,206,211,216,222,228,234,239,245,251,256,262,268,274,280,286,292,298,304,310,315,320,326,332,338],{"__ignoreMap":55},[59,109,110],{"class":61,"line":62},[59,111,112],{},"from pymilvus import AnnSearchRequest, Collection, WeightedRanker\n",[59,114,115],{"class":61,"line":68},[59,116,78],{"emptyLinePlaceholder":77},[59,118,119],{"class":61,"line":74},[59,120,78],{"emptyLinePlaceholder":77},[59,122,123],{"class":61,"line":81},[59,124,125],{},"def hybrid_search(query: str, top_k: int = 20) -> list[dict]:\n",[59,127,128],{"class":61,"line":87},[59,129,130],{},"    collection = Collection(\"rag_docs\")\n",[59,132,134],{"class":61,"line":133},6,[59,135,136],{},"    query_embeddings = ef([query])\n",[59,138,140],{"class":61,"line":139},7,[59,141,78],{"emptyLinePlaceholder":77},[59,143,145],{"class":61,"line":144},8,[59,146,147],{},"    dense_req = AnnSearchRequest(\n",[59,149,151],{"class":61,"line":150},9,[59,152,153],{},"        [query_embeddings[\"dense\"][0]],\n",[59,155,157],{"class":61,"line":156},10,[59,158,159],{},"        \"dense_vector\",\n",[59,161,163],{"class":61,"line":162},11,[59,164,165],{},"        {\"metric_type\": \"IP\", \"params\": {}},\n",[59,167,169],{"class":61,"line":168},12,[59,170,171],{},"        limit=top_k,\n",[59,173,175],{"class":61,"line":174},13,[59,176,177],{},"    )\n",[59,179,181],{"class":61,"line":180},14,[59,182,183],{},"    sparse_req = AnnSearchRequest(\n",[59,185,187],{"class":61,"line":186},15,[59,188,189],{},"        [query_embeddings[\"sparse\"][[0]]],\n",[59,191,193],{"class":61,"line":192},16,[59,194,195],{},"        \"sparse_vector\",\n",[59,197,199],{"class":61,"line":198},17,[59,200,165],{},[59,202,204],{"class":61,"line":203},18,[59,205,171],{},[59,207,209],{"class":61,"line":208},19,[59,210,177],{},[59,212,214],{"class":61,"line":213},20,[59,215,78],{"emptyLinePlaceholder":77},[59,217,219],{"class":61,"line":218},21,[59,220,221],{},"    results = collection.hybrid_search(\n",[59,223,225],{"class":61,"line":224},22,[59,226,227],{},"        [sparse_req, dense_req],\n",[59,229,231],{"class":61,"line":230},23,[59,232,233],{},"        rerank=WeightedRanker(0.7, 1.0),\n",[59,235,237],{"class":61,"line":236},24,[59,238,171],{},[59,240,242],{"class":61,"line":241},25,[59,243,244],{},"        output_fields=[\"text\", \"source\", \"parent_id\"],\n",[59,246,248],{"class":61,"line":247},26,[59,249,250],{},"    )[0]\n",[59,252,254],{"class":61,"line":253},27,[59,255,78],{"emptyLinePlaceholder":77},[59,257,259],{"class":61,"line":258},28,[59,260,261],{},"    return [\n",[59,263,265],{"class":61,"line":264},29,[59,266,267],{},"        {\n",[59,269,271],{"class":61,"line":270},30,[59,272,273],{},"            \"id\": hit.id,\n",[59,275,277],{"class":61,"line":276},31,[59,278,279],{},"            \"text\": hit.get(\"text\"),\n",[59,281,283],{"class":61,"line":282},32,[59,284,285],{},"            \"source\": hit.get(\"source\"),\n",[59,287,289],{"class":61,"line":288},33,[59,290,291],{},"            \"parent_id\": hit.get(\"parent_id\"),\n",[59,293,295],{"class":61,"line":294},34,[59,296,297],{},"        }\n",[59,299,301],{"class":61,"line":300},35,[59,302,303],{},"        for hit in results\n",[59,305,307],{"class":61,"line":306},36,[59,308,309],{},"    ]\n",[59,311,313],{"class":61,"line":312},37,[59,314,78],{"emptyLinePlaceholder":77},[59,316,318],{"class":61,"line":317},38,[59,319,78],{"emptyLinePlaceholder":77},[59,321,323],{"class":61,"line":322},39,[59,324,325],{},"candidates = hybrid_search(\"新员工年假怎么计算？\")\n",[59,327,329],{"class":61,"line":328},40,[59,330,331],{},"print(f\"混合检索召回：{len(candidates)} 个候选片段\")\n",[59,333,335],{"class":61,"line":334},41,[59,336,337],{},"for item in candidates[:3]:\n",[59,339,341],{"class":61,"line":340},42,[59,342,343],{},"    print(item[\"parent_id\"], item[\"text\"][:40])\n",[96,345,346],{},"示例输出：",[50,348,353],{"className":349,"code":351,"language":352,"meta":55},[350],"language-text","混合检索召回：20 个候选片段\nP3C5 第三章 休假制度...\nP3C2 年假计算规则...\nP1C1 新员工入职流程...\n","text",[32,354,351],{"__ignoreMap":55},[96,356,357],{},"说明：",[19,359,360,366,375],{},[22,361,362,365],{},[32,363,364],{},"query_embeddings[\"dense\"][0]"," 取 Dense 查询向量",[22,367,368,371,372],{},[32,369,370],{},"query_embeddings[\"sparse\"][[0]]"," 取 Sparse 查询向量，必须使用二维索引，不能写成 ",[32,373,374],{},"[0]",[22,376,377,380],{},[32,378,379],{},"WeightedRanker(0.7, 1.0)"," 分别对应 Sparse 和 Dense 的权重，这里更依赖语义检索",[10,382,384],{"id":383},"_2-bge-reranker-精排","2. BGE Reranker 精排",[96,386,387],{},"Reranker 对“问题 + 候选片段”逐对打分，从粗排结果中挑出最相关的 Top 5，弥补向量检索只看整体相似度的不足。",[50,389,391],{"className":52,"code":390,"language":54,"meta":55,"style":55},"from sentence_transformers import CrossEncoder\n\nreranker = CrossEncoder(\"BAAI\u002Fbge-reranker-v2-m3\")\npairs = [(query, item[\"text\"]) for item in candidates]\nscores = reranker.predict(pairs)\n\nranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)[:5]\n\nfor rank, (item, score) in enumerate(ranked, start=1):\n    print(f\"{rank}. 得分 {score:.3f} | {item['parent_id']} | {item['text'][:40]}\")\n",[32,392,393,398,402,407,412,417,421,426,430,435],{"__ignoreMap":55},[59,394,395],{"class":61,"line":62},[59,396,397],{},"from sentence_transformers import CrossEncoder\n",[59,399,400],{"class":61,"line":68},[59,401,78],{"emptyLinePlaceholder":77},[59,403,404],{"class":61,"line":74},[59,405,406],{},"reranker = CrossEncoder(\"BAAI\u002Fbge-reranker-v2-m3\")\n",[59,408,409],{"class":61,"line":81},[59,410,411],{},"pairs = [(query, item[\"text\"]) for item in candidates]\n",[59,413,414],{"class":61,"line":87},[59,415,416],{},"scores = reranker.predict(pairs)\n",[59,418,419],{"class":61,"line":133},[59,420,78],{"emptyLinePlaceholder":77},[59,422,423],{"class":61,"line":139},[59,424,425],{},"ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)[:5]\n",[59,427,428],{"class":61,"line":144},[59,429,78],{"emptyLinePlaceholder":77},[59,431,432],{"class":61,"line":150},[59,433,434],{},"for rank, (item, score) in enumerate(ranked, start=1):\n",[59,436,437],{"class":61,"line":156},[59,438,439],{},"    print(f\"{rank}. 得分 {score:.3f} | {item['parent_id']} | {item['text'][:40]}\")\n",[96,441,346],{},[50,443,446],{"className":444,"code":445,"language":352,"meta":55},[350],"1. 得分 0.921 | P3C5 | 正式员工入职满一年后，每年可享受 5 个工作日年假...\n2. 得分 0.886 | P3C2 | 年假按自然年度计算，未休完部分可顺延至次年第一季度...\n3. 得分 0.701 | P1C1 | 新员工入职流程：...\n",[32,447,445],{"__ignoreMap":55},[96,449,357,450,453],{},[32,451,452],{},"reranker"," 会在后面的生成步骤中继续复用，因此本示例和下一步示例需要放在同一个 Python 进程里按顺序运行。",[10,455,457],{"id":456},"_3-上下文构建与-dashscope-流式生成","3. 上下文构建与 DashScope 流式生成",[96,459,460,461,464],{},"生成阶段把精排后的片段整理成带编号的参考资料，要求模型只依据资料回答，并在末尾用 ",[32,462,463],{},"[编号]"," 标注依据。",[50,466,468],{"className":52,"code":467,"language":54,"meta":55,"style":55},"import os\n\nimport dashscope\nfrom dashscope import Generation\n\n\ndef build_context(items: list[tuple[dict, float]]) -> str:\n    blocks = []\n    for index, (item, _score) in enumerate(items, start=1):\n        blocks.append(f\"[{index}] {item['text']}\\n来源：{item['source']}\")\n    return \"\\n\\n\".join(blocks)\n\n\ndef answer(question: str) -> str:\n    candidates = hybrid_search(question)\n    ranked = sorted(\n        zip(candidates, reranker.predict([(question, item[\"text\"]) for item in candidates])),\n        key=lambda x: x[1],\n        reverse=True,\n    )[:5]\n    context = build_context(ranked)\n\n    prompt = f\"\"\"请仅根据以下资料回答问题。\n\n资料：\n{context}\n\n问题：{question}\n\n要求：\n1. 如果资料中没有答案，请直接说明“资料中未找到相关信息”。\n2. 回答末尾用 [编号] 标注依据，例如 [1]。\n\"\"\"\n\n    responses = Generation.call(\n        model=\"qwen-plus\",\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n        stream=True,\n        incremental_output=True,\n    )\n\n    answer_text = \"\"\n    for response in responses:\n        if response.status_code == 200:\n            piece = response.output.text\n            answer_text += piece\n            print(piece, end=\"\", flush=True)\n    print()\n    return answer_text\n\n\nanswer(\"新员工入职满一年能休几天年假？\")\n",[32,469,470,475,479,484,489,493,497,502,507,512,517,522,526,530,535,540,545,550,555,560,565,570,574,579,583,588,593,597,602,606,611,616,621,626,630,635,640,645,650,655,659,663,668,674,680,686,692,698,704,710,715,720],{"__ignoreMap":55},[59,471,472],{"class":61,"line":62},[59,473,474],{},"import os\n",[59,476,477],{"class":61,"line":68},[59,478,78],{"emptyLinePlaceholder":77},[59,480,481],{"class":61,"line":74},[59,482,483],{},"import dashscope\n",[59,485,486],{"class":61,"line":81},[59,487,488],{},"from dashscope import Generation\n",[59,490,491],{"class":61,"line":87},[59,492,78],{"emptyLinePlaceholder":77},[59,494,495],{"class":61,"line":133},[59,496,78],{"emptyLinePlaceholder":77},[59,498,499],{"class":61,"line":139},[59,500,501],{},"def build_context(items: list[tuple[dict, float]]) -> str:\n",[59,503,504],{"class":61,"line":144},[59,505,506],{},"    blocks = []\n",[59,508,509],{"class":61,"line":150},[59,510,511],{},"    for index, (item, _score) in enumerate(items, start=1):\n",[59,513,514],{"class":61,"line":156},[59,515,516],{},"        blocks.append(f\"[{index}] {item['text']}\\n来源：{item['source']}\")\n",[59,518,519],{"class":61,"line":162},[59,520,521],{},"    return \"\\n\\n\".join(blocks)\n",[59,523,524],{"class":61,"line":168},[59,525,78],{"emptyLinePlaceholder":77},[59,527,528],{"class":61,"line":174},[59,529,78],{"emptyLinePlaceholder":77},[59,531,532],{"class":61,"line":180},[59,533,534],{},"def answer(question: str) -> str:\n",[59,536,537],{"class":61,"line":186},[59,538,539],{},"    candidates = hybrid_search(question)\n",[59,541,542],{"class":61,"line":192},[59,543,544],{},"    ranked = sorted(\n",[59,546,547],{"class":61,"line":198},[59,548,549],{},"        zip(candidates, reranker.predict([(question, item[\"text\"]) for item in candidates])),\n",[59,551,552],{"class":61,"line":203},[59,553,554],{},"        key=lambda x: x[1],\n",[59,556,557],{"class":61,"line":208},[59,558,559],{},"        reverse=True,\n",[59,561,562],{"class":61,"line":213},[59,563,564],{},"    )[:5]\n",[59,566,567],{"class":61,"line":218},[59,568,569],{},"    context = build_context(ranked)\n",[59,571,572],{"class":61,"line":224},[59,573,78],{"emptyLinePlaceholder":77},[59,575,576],{"class":61,"line":230},[59,577,578],{},"    prompt = f\"\"\"请仅根据以下资料回答问题。\n",[59,580,581],{"class":61,"line":236},[59,582,78],{"emptyLinePlaceholder":77},[59,584,585],{"class":61,"line":241},[59,586,587],{},"资料：\n",[59,589,590],{"class":61,"line":247},[59,591,592],{},"{context}\n",[59,594,595],{"class":61,"line":253},[59,596,78],{"emptyLinePlaceholder":77},[59,598,599],{"class":61,"line":258},[59,600,601],{},"问题：{question}\n",[59,603,604],{"class":61,"line":264},[59,605,78],{"emptyLinePlaceholder":77},[59,607,608],{"class":61,"line":270},[59,609,610],{},"要求：\n",[59,612,613],{"class":61,"line":276},[59,614,615],{},"1. 如果资料中没有答案，请直接说明“资料中未找到相关信息”。\n",[59,617,618],{"class":61,"line":282},[59,619,620],{},"2. 回答末尾用 [编号] 标注依据，例如 [1]。\n",[59,622,623],{"class":61,"line":288},[59,624,625],{},"\"\"\"\n",[59,627,628],{"class":61,"line":294},[59,629,78],{"emptyLinePlaceholder":77},[59,631,632],{"class":61,"line":300},[59,633,634],{},"    responses = Generation.call(\n",[59,636,637],{"class":61,"line":306},[59,638,639],{},"        model=\"qwen-plus\",\n",[59,641,642],{"class":61,"line":312},[59,643,644],{},"        messages=[{\"role\": \"user\", \"content\": prompt}],\n",[59,646,647],{"class":61,"line":317},[59,648,649],{},"        stream=True,\n",[59,651,652],{"class":61,"line":322},[59,653,654],{},"        incremental_output=True,\n",[59,656,657],{"class":61,"line":328},[59,658,177],{},[59,660,661],{"class":61,"line":334},[59,662,78],{"emptyLinePlaceholder":77},[59,664,665],{"class":61,"line":340},[59,666,667],{},"    answer_text = \"\"\n",[59,669,671],{"class":61,"line":670},43,[59,672,673],{},"    for response in responses:\n",[59,675,677],{"class":61,"line":676},44,[59,678,679],{},"        if response.status_code == 200:\n",[59,681,683],{"class":61,"line":682},45,[59,684,685],{},"            piece = response.output.text\n",[59,687,689],{"class":61,"line":688},46,[59,690,691],{},"            answer_text += piece\n",[59,693,695],{"class":61,"line":694},47,[59,696,697],{},"            print(piece, end=\"\", flush=True)\n",[59,699,701],{"class":61,"line":700},48,[59,702,703],{},"    print()\n",[59,705,707],{"class":61,"line":706},49,[59,708,709],{},"    return answer_text\n",[59,711,713],{"class":61,"line":712},50,[59,714,78],{"emptyLinePlaceholder":77},[59,716,718],{"class":61,"line":717},51,[59,719,78],{"emptyLinePlaceholder":77},[59,721,723],{"class":61,"line":722},52,[59,724,725],{},"answer(\"新员工入职满一年能休几天年假？\")\n",[96,727,346],{},[50,729,732],{"className":730,"code":731,"language":352,"meta":55},[350],"根据《员工手册》第三章，正式员工入职满一年后，每年可享受 5 个工作日年假；年假按自然年度计算，未休完部分可顺延至次年第一季度 [1][2]。\n",[32,733,731],{"__ignoreMap":55},[96,735,357],{},[19,737,738,748,755],{},[22,739,740,743,744,747],{},[32,741,742],{},"stream=True"," 开启流式输出，",[32,745,746],{},"incremental_output=True"," 让每个分片只包含新增文本",[22,749,750,751,754],{},"最终返回的 ",[32,752,753],{},"answer_text"," 是完整答案，可保存到数据库或返回给前端",[22,756,757,758,760],{},"如果希望答案严格可溯源，可以把 ",[32,759,463],{}," 解析出来，再映射到原始文档页码或文件路径",[762,763,764],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":55,"searchDepth":68,"depth":68,"links":766},[767,768,769,770,771],{"id":12,"depth":68,"text":13},{"id":16,"depth":68,"text":17},{"id":93,"depth":68,"text":94},{"id":383,"depth":68,"text":384},{"id":456,"depth":68,"text":457},"2026-05-19","Dense + Sparse 混合检索、Reranker 精排与流式生成，覆盖 RAG 在线链路的完整实战。",false,"md","article",{"migration":778},{"generator":779,"sourceSha256":780},"nuxt-site\u002Fscripts\u002Fmigrate-content.mjs","6eef27c9484690d14282742484446d435038ed607a5dfe8871ac72505e300789","\u002Frag\u002F07_rag_在线链路",null,"rag",{"title":5,"description":773},"content\u002Frag\u002F07_RAG_在线链路.md","rag\u002F07_RAG_在线链路",[788,789,790,791],"RAG","Milvus","混合检索","重排","Gr3vz4jf5phP58zwF49DljSr2IHqhTvXxBO5bndx1HA",1791279145549]