查询、检索与生成
编写查询流程,为用户问题生成嵌入,检索排名前 k 的分块,格式化增强提示,调用 LLM,并返回带引用的答案。
查询、检索与生成 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。
查询流程:端到端
查询流程是RAG 的在线部分,也就是用户提出问题时实时运行的代码。它连接了建立 Index 期间构建的所有组件:嵌入模型、向量存储、提示模板和 LLM。实现良好的查询流程在大多数工作负载下都能在 500 毫秒内完成,并生成有依据且带引用的答案。本课将从零开始构建每个步骤。
步骤 1:嵌入用户查询
第一步是使用建立 Index 时采用的同一个模型,将用户的自然语言问题转换为向量嵌入。这个嵌入会编码问题的语义,并与向量存储中的文档分块嵌入进行比较。请让这一步保持快速——使用 text-embedding-3-small 之类的轻量模型,并缓存重复相同查询的嵌入。
from openai import OpenAI
client = OpenAI()
def embed_query(question: str) -> list:
response = client.embeddings.create(
model='text-embedding-3-small',
input=[question]
)
return response.data[0].embedding
user_question = 'What is our remote work policy?'
query_vector = embed_query(user_question)
print(f'Query embedded: {len(query_vector)}-dim vector')步骤 2:检索前 K 个分块
将查询向量发送到向量存储,以查找语义最相似的 K 个分块。返回的匹配结果会按照余弦相似度分数排序(通常为 0.0 到 1.0,数值越高越好)。理想的 K 值需要在上下文丰富度与上下文窗口成本之间取得平衡:K=5 是常见的起始值。您还可以在此处应用元数据筛选条件,将检索范围限制在特定部门、文档类型或日期范围内。
def retrieve_chunks(query_vector, index, top_k=5, filters=None):
query_params = {
'vector': query_vector,
'top_k': top_k,
'include_metadata': True
}
if filters:
query_params['filter'] = filters
results = index.query(**query_params)
chunks = []
for match in results.matches:
chunks.append({
'score': match.score,
'text': match.metadata['text'],
'source': match.metadata.get('source', ''),
'page': match.metadata.get('page', '')
})
return chunks步骤 3:按分数阈值筛选
并非所有检索到的分块都真正相关——有些分块的相似度分数可能很低,但由于查询超出了 Index 的覆盖范围,仍然排在前 K 个结果中。请应用最低分数阈值,筛除置信度较低的匹配结果。如果所有检索到的分块都低于该阈值,应返回“未找到相关信息”,而不是将无关上下文发送给 LLM;后者生成的答案会比礼貌地拒绝回答更糟。
MIN_SCORE_THRESHOLD = 0.75
def filter_by_score(chunks, threshold=MIN_SCORE_THRESHOLD):
relevant = [c for c in chunks if c['score'] >= threshold]
if not relevant:
print(f'No chunks above threshold {threshold}. Scores: {[c["score"] for c in chunks]}')
return relevant
retrieved = retrieve_chunks(query_vector, index, top_k=5)
filtered = filter_by_score(retrieved)
if not filtered:
print('Responding: no relevant information found')步骤 4:格式化上下文块
将检索到的分块组装成 LLM 将要读取的结构化上下文块。为每个分块标注来源,以便模型准确引用。请在分块之间添加分隔符,提升可读性。将总上下文控制在令牌预算以内——使用 tiktoken 统计令牌数;如果超出限制,则截断或删除分数较低的分块。上下文块会插入提示中,位置在系统指令与用户问题之间。
def format_context(chunks):
parts = []
for i, chunk in enumerate(chunks, start=1):
source_label = chunk['source']
if chunk.get('page'):
source_label += f", page {chunk['page']}"
parts.append(
f'[Document {i} | Source: {source_label}]\n{chunk["text"]}'
)
return '\n\n---\n\n'.join(parts)
context = format_context(filtered)
print(f'Context block: {len(context)} characters')步骤 5:构建增强提示
将上下文块、系统指令和用户问题组合成最终提示。系统消息会告诉模型只能使用提供的上下文,并且要引用来源。用户消息包含格式化后的上下文,后面接着问题。这种清晰的分隔可以防止模型将上下文内容与问题混淆,也明确划分了检索数据和用户输入之间的边界。
def build_prompt(question, context):
system_message = (
'You are a helpful assistant. Answer the question using ONLY '
'the information in the provided documents. '
'Cite the document number(s) used, like [Doc 1]. '
'If the documents do not contain the answer, say so.'
)
user_message = (
f'Documents:\n\n{context}\n\n'
f'Question: {question}'
)
return system_message, user_message步骤 6:调用 LLM 并获取答案
使用 Chat Completions API 将组装好的提示发送给 LLM。在事实性问答中使用较低的温度(0.0 到 0.3),以获得一致且有依据的答案。较高的温度会产生更具创造性的回答,但也会增加模型添加上下文之外信息的风险。解析响应,同时返回答案文本和检索到的来源,以便应用向用户展示引用。
def generate_answer(question, context, sources):
system_msg, user_msg = build_prompt(question, context)
response = client.chat.completions.create(
model='gpt-4o',
temperature=0.1, # low temperature for factual Q&A
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
answer = response.choices[0].message.content
return {
'answer': answer,
'sources': sources,
'tokens_used': response.usage.total_tokens
}整合全部步骤
完整的查询流程会依次调用这些步骤。每一步都是可以独立测试的纯函数,数据也会从一个步骤清晰地流向下一个步骤。在每一步添加日志记录,可以让流程具备可观测性——您能够准确查看检索到了哪些分块、它们的分数是多少、上下文是如何组装的,以及使用了多少令牌。这种可见性对于调试和提升检索质量至关重要。
def answer_question(user_question, vector_index):
# Step 1: Embed query
q_vector = embed_query(user_question)
# Step 2: Retrieve
chunks = retrieve_chunks(q_vector, vector_index, top_k=5)
# Step 3: Filter low-confidence matches
chunks = filter_by_score(chunks, threshold=0.70)
if not chunks:
return {'answer': 'I do not have information about that topic.', 'sources': []}
# Step 4 & 5: Format and build prompt
context = format_context(chunks)
sources = [c['source'] for c in chunks]
# Step 6: Generate
return generate_answer(user_question, context, sources)延迟优化
查询流程包含两个受输入/输出限制的步骤:嵌入调用和 LLM 调用。请让它们避免不必要的等待:嵌入调用速度很快(<100ms),LLM 调用则较慢(500ms-3s)。为了降低用户感知到的延迟,请流式传输 LLM 响应,让令牌在生成后立即显示,而不是等待完整响应。对于重复的相同查询,请缓存其嵌入,以避免重复调用 API。
async def answer_question_streaming(question, index):
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
if not chunks:
yield 'I do not have information about that topic.'
return
context = format_context(chunks)
system_msg, user_msg = build_prompt(question, context)
stream = await client.chat.completions.create(
model='gpt-4o',
stream=True,
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
async for chunk in stream:
delta = chunk.choices[0].delta.content or ''
yield delta通过日志记录实现可观测性
生产环境中的 RAG 流程需要结构化日志记录,以便您诊断检索失败或 LLM 生成错误答案的情况。请为每个请求记录查询、检索到的分块 ID 和分数、上下文令牌数、答案以及延迟。将这些日志存储在数据库或可观测性平台中。当用户报告错误答案时,您可以重放完全相同的查询,并检查检索到了哪些分块,以及它们为何不足以支持答案。
import time
import logging
import json
def answer_question_with_logging(question, index):
start = time.time()
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
context = format_context(chunks)
result = generate_answer(question, context, [c['source'] for c in chunks])
latency_ms = (time.time() - start) * 1000
log_entry = {
'question': question,
'num_chunks_retrieved': len(chunks),
'chunk_scores': [c['score'] for c in chunks],
'tokens_used': result.get('tokens_used'),
'latency_ms': round(latency_ms)
}
logging.info(json.dumps(log_entry))
return result缓存查询嵌入
如果您的应用收到大量重复或近似相同的查询——例如 FAQ 机器人中用户经常提出相同问题——那么缓存查询嵌入是一种简单且效果显著的优化方式。对查询字符串进行哈希处理,在 Redis 缓存中查找对应的嵌入,只有缓存未命中时才调用嵌入 API。在生产环境的 FAQ 和支持聊天机器人中,嵌入缓存命中率达到 30-60% 很常见;这可以大幅减少 API 成本,并让每次命中缓存的查询减少 50-100 毫秒的延迟。
import hashlib
import json
import redis
r = redis.Redis(host='localhost', port=6379)
EMBED_CACHE_TTL = 86400 # 24 hours
def embed_query_cached(question):
cache_key = 'embed:' + hashlib.sha256(question.encode()).hexdigest()
cached = r.get(cache_key)
if cached:
return json.loads(cached) # cache hit
# Cache miss: call the API
vector = embed_query(question)
r.setex(cache_key, EMBED_CACHE_TTL, json.dumps(vector))
return vector快速检查
测试您对本课 AI 工程概念的理解。
课程回顾
本课您学习了:六步查询流程(嵌入查询、检索分块、按分数筛选、格式化上下文、构建提示、生成答案);使用分数阈值筛选处理超出 Index 覆盖范围的查询;以及包括响应流式传输、结构化日志记录和延迟优化在内的生产环境增强措施。接下来,我们将学习如何评估完整的 RAG 系统是否真正正常工作。
常见问题解答
「查询、检索与生成」课时是免费的吗?
是的 — 「查询、检索与生成」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。
「查询、检索与生成」这节课中我会学到什么?
编写查询流程,为用户问题生成嵌入,检索排名前 k 的分块,格式化增强提示,调用 LLM,并返回带引用的答案。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 AI Engineering Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「查询、检索与生成」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 AI Engineering Academy 课中编写并运行代码吗?
能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。