实用提示:我删除了向量数据库,RAG系统的性能反而更好了
《实用笔记》操作指南:我删除了向量数据库,RAG系统性能反而更好了——面向采用该模式的团队提供的合同、检查清单及可直接使用的代码模板。
可将此内容视为《我删除了向量数据库,RAG系统反而变好了》一文中理念面向操作人员的重构版本:清晰的阶段划分、有序的代码模块,以及便于交接的恢复说明。 在“概览”阶段,若将其视为可度量的界面来处理效果最佳。在扩大范围之前,先记录一份理想的操作流程、一个故障案例以及回滚说明。 需同时记录正常运行路径和恢复路径。重试机制、人工审核环节以及死信处理都是产品本身的一部分,而非后续需要补充的内容。
什么是RAG?为何你应当关注它?
在修改代码之前,对于“什么是RAG”以及相关阶段,需明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。相比冗长的脚本,更应采用小型、可测试的单元。当某个步骤失败时,故障原因应能指向单一责任主体,而非复杂的流程链。必须引用实际作为答案依据的段落;没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失所致。
最初显而易见的想法(以及它为何行不通)
在“显而易见的初步设想”阶段,应在修改代码之前明确输入内容、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,定义成功判定标准,并拒绝默许的半完成状态。 需引用实际作为答案依据的段落。没有引用的话,操作人员就无法区分幻觉内容与索引缺失的情况。
from openai import OpenAI
import PyPDF2
client = OpenAI(api_key="your-api-key")
# Extract all text from a PDF
def extract_pdf_text(pdf_path):
reader = PyPDF2.PdfReader(pdf_path)
full_text = ""
for page in reader.pages:
full_text += page.extract_text() + "\n"
return full_text
document_text = extract_pdf_text("annual_report.pdf")
user_question = "What was the total revenue in 2024?"
response = client.chat.completions.create(
model="gpt-4.1",
messages=[
{"role": "system", "content": "Answer based on the provided document."},
{"role": "user", "content": f"Document:\n{document_text}\n\nQuestion: {user_question}"}
]
)
print(response.choices[0].message.content)
传统向量RAG:当前的行业标准
在传统向量RAG阶段,修改代码之前需明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 在功能结果旁记录执行时间以及令牌或查询成本。提前显示成本可避免在流程从演示环境转向共享环境时出现意外费用。 需注明实际用于生成答案的文本片段。没有引用依据的话,操作人员就无法区分幻觉内容与索引缺失问题。 在传统向量RAG阶段,修改代码之前需明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程与异常恢复流程。重试机制、人工审核环节以及错误处理措施都是产品本身的组成部分,而非后续需要补充的内容。
/p>from openai import OpenAI
import chromadb
import PyPDF2
client = OpenAI(api_key="your-api-key")
# Step 1: Extract and chunk the document
def extract_and_chunk(pdf_path, chunk_size=500):
reader = PyPDF2.PdfReader(pdf_path)
full_text = ""
for page in reader.pages:
full_text += page.extract_text() + "\n"
words = full_text.split()
chunks = []
for i in range(0, len(words), chunk_size):
chunk = " ".join(words[i:i + chunk_size])
chunks.append(chunk)
return chunks
chunks = extract_and_chunk("annual_report.pdf")
# Step 2 and 3: Embed and store in ChromaDB
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("my_documents")
collection.add(
documents=chunks,
ids=[f"chunk_{i}" for i in range(len(chunks))]
)
# Step 4: Search for relevant chunks
user_question = "What was the total revenue in 2024?"
results = collection.query(
query_texts=[user_question],
n_results=5
)
relevant_chunks = "\n\n".join(results["documents"][0])
# Step 5: Generate answer with focused context
response = client.chat.completions.create(
model="gpt-4.1",
messages=[
{"role": "system", "content": "Answer based only on the provided context."},
{"role": "user", "content": f"Context:\n{relevant_chunks}\n\nQuestion: {user_question}"}
]
)
print(response.choices[0].message.content)
向量RAG的缺陷所在
在处理“向量RAG的缺陷所在”这一阶段时,首先需明确相关约定:所需的输入参数、成功标志,以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 建议采用小型、可测试的单元而非庞大的脚本。当某个步骤出现故障时,故障应指向单一的责任模块,而非复杂的流程链。 在调整提示词之前,先使用固定的问题集来测试召回率。仅仅更换提示词很难解决检索效果不佳的问题。
问题1:分块处理会破坏上下文
在处理“问题1:分块导致信息丢失”这一阶段时,首先写下相关契约:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 将这一阶段视为输入与验证后输出之间的契约。为相关产物命名,明确成功判定标准,杜绝无声的半完成状态。 在调整提示词之前,先使用固定的问题集来测试召回率。仅仅更换提示词很难改善较差的信息检索效果。
paragraph = """The company's Q3 operating income was $4.2 billion,
representing a 12% increase over the prior year period. This growth
was primarily driven by the expansion of cloud services, which
contributed $2.8 billion in recurring revenue as detailed in the
segment breakdown in Appendix C."""
# Simulating a chunk boundary at word 20
words = paragraph.split()
chunk_1 = " ".join(words[:20])
chunk_2 = " ".join(words[20:])
print("Chunk 1:", chunk_1)
print("---")
print("Chunk 2:", chunk_2)
问题2:交叉引用丢失
在处理“问题2:交叉引用获取”阶段时,首先需明确相关约定:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在系统从演示环境转向共享环境时出现意外费用。 在调整提示词之前,先使用固定的问题集测试系统的召回率。仅仅更换提示词往往无法改善较差的检索效果。 在处理“问题2:交叉引用获取”阶段时,首先需明确相关约定:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理措施都是产品本身的组成部分,而非后续需要补充的功能。
# Simulating a cross-reference failure
documents = {
"page_12": "The company's debt-to-equity ratio improved significantly in 2024. For a full breakdown of long-term obligations, see Appendix G on page 87.",
"page_45": "Marketing expenses increased by 15% due to new campaigns.",
"page_87": "Appendix G: Long-term debt stands at $12.4B. Senior notes: $8.1B. Credit facility: $4.3B. Maturity schedule: 2026-2034."
}
# Vector RAG would likely return page_12 for a debt question
# but miss page_87 where the actual numbers live
# because "debt-to-equity ratio" is more similar to the query
# than "Senior notes" and "Credit facility"
问题3:用户用词的重要性被过度强调了
在处理“问题3——用户用词”阶段时,若能将其视为可测量的对象,则效果最佳。在扩大范围之前,先收集一份优秀的文本样本、一个失败案例以及回滚说明。 相较于庞大的脚本,应优先选择小型且可测试的单元。当某个步骤出现故障时,故障原因应能指向单一责任主体,而非复杂的流程链。 应将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应迫使另一项也必须重新编写。
引入无向量RAG:教会AI像人类一样阅读
将“无向量 RAG 教学”阶段视为可度量的对象来处理效果最佳。在扩大范围之前,先记录一份优秀的示例文本、一个失败案例以及回滚说明。 把这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,绝不允许出现无声无息的半完成状态。 将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应强制要求重新编写另一项。
步骤 1:构建分层树形索引
将“第一步:构建测试环境”视为一个可度量的对象来处理,效果最佳。在扩大范围之前,需记录一份理想的测试用例、一个故障案例以及回滚说明。在功能结果旁还需记录执行时间以及令牌或查询成本。提前了解这些成本,就能避免在系统从演示环境过渡到共享环境时出现意外费用。应将分块策略与检索策略分开,当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。将“第一步:构建测试环境”视为一个可度量的对象来处理,效果最佳。在扩大范围之前,需记录一份理想的测试用例、一个故障案例以及回滚说明。需同时记录正常流程和恢复流程的文档,重试机制、人工审核环节以及死信处理都属于产品本身的功能,而非后续需要补充的内容。
Root: Annual Report 2024
|-- Executive Summary (pages 1-3)
| Summary: "Overview of company performance, key metrics..."
|-- Financial Statements (pages 15-45)
| |-- Income Statement (pages 15-20)
| |-- Balance Sheet (pages 21-30)
| +-- Cash Flow Statement (pages 31-45)
|-- Risk Factors (pages 46-60)
+-- Appendices (pages 80-120)
|-- Appendix A: Segment Data (pages 80-95)
+-- Appendix G: Detailed Tables (pages 96-120)
第二步:基于推理的树形搜索
在基于推理的树形结构的第二阶段,应在修改代码之前明确输入参数、该阶段的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该阶段,而无需猜测隐藏状态。 相较于冗长的脚本,更应采用小型且可测试的单元。当某个阶段出现故障时,故障原因应能指向单一责任模块,而非复杂的流程链。 若后续步骤为代码或工具调用,相较于自由形式的文字描述,更应使用具有架构验证的结构化输出。
import json
from openai import OpenAI
import PyPDF2
client = OpenAI(api_key="your-api-key")
# Extract text page by page
def extract_pages(pdf_path):
reader = PyPDF2.PdfReader(pdf_path)
pages = {}
for i, page in enumerate(reader.pages):
pages[i + 1] = page.extract_text()
return pages
pages = extract_pages("annual_report.pdf")
all_text = "\n".join([f"--- Page {k} ---\n{v}" for k, v in pages.items()])
# Step 1: Build the tree index using AI reasoning
tree_prompt = f"""You are a document analyst. Read this document and create
a hierarchical table of contents as a JSON tree. Each node should have:
- "title": section name
- "summary": 2-3 sentence description of what this section covers
- "pages": [start_page, end_page]
- "children": array of child nodes (or empty array)
Document:
{all_text[:80000]}
Return ONLY valid JSON. No other text."""
tree_response = client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": tree_prompt}],
response_format={"type": "json_object"}
)
document_tree = json.loads(tree_response.choices[0].message.content)
print("Document tree built successfully!")
print(json.dumps(document_tree, indent=2)[:500])
# Step 2: Reasoning-based search over the tree
user_question = "What was the year-over-year change in operating margin?"
search_prompt = f"""You are a retrieval expert. Given a user question and
a document's table of contents tree, identify which sections are most
likely to contain the answer.
Think step by step:
1. What kind of information does the question ask for?
2. Which sections would a human expert check first?
3. Are there sections that might cross-reference each other?
Document tree:
{json.dumps(document_tree, indent=2)}
Question: {user_question}
Return JSON with:
- "reasoning": your step-by-step thought process
- "relevant_pages": list of page numbers to retrieve"""
search_response = client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": search_prompt}],
response_format={"type": "json_object"}
)
search_result = json.loads(search_response.choices[0].message.content)
print("Reasoning:", search_result["reasoning"])
# Step 3: Fetch only relevant pages and generate answer
relevant_text = ""
for page_num in search_result["relevant_pages"]:
if page_num in pages:
relevant_text += f"\n--- Page {page_num} ---\n{pages[page_num]}"
answer_response = client.chat.completions.create(
model="gpt-4.1",
messages=[
{"role": "system", "content": "Answer precisely based on the document context provided."},
{"role": "user", "content": f"Context:{relevant_text}\n\nQuestion: {user_question}"}
]
)
print("\nAnswer:", answer_response.choices[0].message.content)
利用 PageIndex SDK 实现生产级准备
在“准备投入生产并设置页码索引”阶段,应在修改代码之前明确输入参数、该步骤的负责人以及完成标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 应将此阶段视为输入与已验证输出之间的契约。为相关成果命名,定义成功判定标准,并杜绝无声的半完成状态。 需引用实际作为答案依据的段落。没有引用的话,操作人员就无法区分是幻觉还是索引缺失。
from pageindex import PageIndexClient
import time
# Initialize the client
pi_client = PageIndexClient(api_key="your-pageindex-api-key")
# Upload and process your document
result = pi_client.submit_document("./annual_report.pdf")
doc_id = result["doc_id"]
# Wait for processing to complete
while True:
status = pi_client.get_document(doc_id)["status"]
if status == "completed":
print("Document processed!")
break
time.sleep(5)
# Inspect the generated tree structure
tree_result = pi_client.get_tree(doc_id)
if tree_result.get("status") == "completed":
tree = tree_result["result"]
for node in tree:
print(f"[{node['node_id']}] {node['title']} - Page {node['page_index']}")
# Ask questions using the Chat API
response = pi_client.chat_completions(
messages=[{"role": "user", "content": "What was total revenue in FY2024 vs FY2023?"}],
doc_id=doc_id
)
print(response["choices"][0]["message"]["content"])
{
"title": "Financial Stability",
"node_id": "0006",
"page_index": 21,
"text": "The Federal Reserve maintains financial stability...",
"nodes": [
{
"title": "Monitoring Financial Vulnerabilities",
"node_id": "0007",
"page_index": 22,
"text": "The Federal Reserve's monitoring focuses on..."
},
{
"title": "Domestic and International Cooperation",
"node_id": "0008",
"page_index": 28,
"text": "In 2023, the Federal Reserve collaborated..."
}
]
}
response = pi_client.chat_completions(
messages=[{"role": "user", "content": "Compare the results across these two reports."}],
doc_id=["pi-abc123def456", "pi-abc123ghi789"]
)
数据讲述真相
在修改代码之前,对于“数据说明阶段”,需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 应在功能结果旁记录执行时间以及令牌或查询成本。提前显示成本可避免在流程从演示环境转向共享环境时出现意外费用。 需注明实际作为答案依据的段落。没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。 在修改代码之前,对于“数据说明阶段”,需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理方式都是产品本身的组成部分,而非后续需要补充的内容。
何时使用哪种方法
在处理“何时使用哪种方法”这一阶段时,首先写下相关契约:所需的输入、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 优先选择小型、可测试的单元,而非庞大的脚本。当某个步骤失败时,故障应指向单一的责任模块,而非复杂的流程链。 在调整提示词之前,先使用固定的问题集来衡量召回率。仅仅更换提示词很难解决检索效果不佳的问题。
接下来该做什么?
在处理“接下来该做什么”这一阶段时,首先写下契约内容:所需的输入、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 将这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功判定标准,并杜绝无声的半完成状态。 在调整提示词之前,先使用固定的问题集来测试召回率。仅仅更换提示词很难改善较差的检索效果。
总结:
在完成ReCap阶段时,首先需写下接口规范:所需的输入参数、成功标识以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在系统从演示环境过渡到共享环境时出现意外账单。 在调整提示词之前,先使用固定的问题集测试系统的召回率。仅仅更换提示词往往无法改善较差的检索效果。 在完成ReCap阶段时,首先需写下接口规范:所需的输入参数、成功标识以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理措施都是产品本身的组成部分,而非后续需要补充的功能。
操作检查清单
在处理操作检查清单阶段时,首先写下合同条款:所需的输入参数、成功标志以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。
将配置信息与应用程序代码分开。环境文件、密钥存储以及功能开关应集中存放于一个位置,以便操作人员无需查看整个系统结构即可进行审计。
在调整提示词之前,先使用固定的问题集来衡量检索覆盖率。仅仅更换提示词很难解决检索效果不佳的问题。
锁定依赖项的版本,并记录用于演示的图像摘要。可重复性比经验知识更为重要。
同时记录正常流程和故障恢复流程。重试机制、人工审核环节以及错误处理方式都是产品的一部分,而非后续需要补充的内容。
在调整提示词之前,先使用固定的问题集来衡量检索覆盖率。仅仅更换提示词很难解决检索效果不佳的问题。
在推广该技术栈之前,应先冻结版本,为关键流程记录标准输出日志,并明确回滚步骤。共享环境需要设置速率限制、租户验证机制,以及负责密钥轮换的明确责任人。与其展示花哨的一次性演示,不如追求扎实可靠的性能。
关于61253a21aab9的批量处理说明:请将服务提供商密钥移出代码仓库,设定单会话令牌使用上限,并将日志存储在评估用测试文件旁,以便后续模型更换时保持数据可比性。
在处理强化安全性的第0阶段时,首先需明确相关规范:所需输入参数、成功标识,以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改始终符合要求。同时要在功能测试结果旁记录执行时间以及令牌或查询成本,提前了解成本情况可避免从演示环境过渡到共享环境时出现意外费用。
强化细节 0/802:为该记录测量处理时间、错误类型以及令牌消耗情况,然后依据固定的问题集而非个别案例来决定是否保留该变更。
将强化笔记的第一阶段视为可度量的对象处理效果最佳。在扩大范围之前,需记录一份理想的操作流程、一个故障案例以及回滚说明。同时记录正常流程与恢复流程。重试机制、人工审核环节以及死信处理都是产品本身的组成部分,而非后续的优化内容。
强化细节 1/802:为该记录测量处理时间、错误类型以及令牌消耗情况,然后依据固定的问题集而非个别案例来决定是否保留该变更。
在强化措施的第2阶段,应在修改代码之前明确输入参数、该步骤的负责人以及完成标准。操作人员应能够从已知的检查点重新执行该步骤,而无需猜测隐藏状态。应将此阶段视为输入与验证后输出之间的契约:为相关成果命名,定义成功判定标准,并拒绝默许部分完成的情况。
强化措施细节2/802:需记录该步骤的耗时、错误类型以及令牌消耗情况,然后依据固定的评估标准而非主观判断来决定是否保留该变更。
在处理强化措施的第3阶段时,首先写下相关约定:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改始终符合规范。 应将配置信息与应用程序代码分开。环境文件、密钥存储以及功能开关应集中存放于一个位置,这样操作人员无需查看整个系统结构即可进行审计。
强化措施细节3/802:针对该措施需统计执行耗时、错误类型以及令牌使用情况,然后依据固定的评估标准而非个人经验来决定是否保留该变更。
将强化措施的第4阶段视为可度量的对象来处理效果最佳。在扩大范围之前,先记录一份标准操作流程、一个失败案例以及回滚说明。 相比复杂的脚本,更应采用小型且可测试的单元。当某个步骤出现故障时,故障原因应能明确指向某个具体职责,而非整个混乱的流程链。
强化措施细节4/802:为该记录测量运行时间、错误类型以及代币消耗情况,然后依据固定的问题集而非个人经验来判断是否保留该变更。
在强化措施的第5阶段,应在修改代码之前明确输入参数、该步骤的负责人以及完成标准。操作人员应能够从已知的检查点重新执行该步骤,而无需猜测隐藏状态。应在功能结果旁记录运行时间以及代币或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。
强化措施细节5/802:为该记录测量运行时间、错误类型以及代币消耗情况,然后依据固定的问题集而非个人经验来判断是否保留该变更。