LLM Zoomcamp RAG 提示词构建指南:从检索结果到可落地的 Instructions 与 User Prompt
【免费下载链接】llm-zoomcampLLM Zoomcamp - a free online course about real-life applications of LLMs. In 10 weeks you will learn how to build an AI system that answers questions about your knowledge base. Register here 👇🏼项目地址: https://gitcode.com/GitHub_Trending/ll/llm-zoomcamp
导读
本文是 LLM Zoomcamp(2026 届)模块 1「Agentic RAG」第 6 课《Building the Prompt》的技术详解。检索(Search)负责把知识库中最相关的文档找出来,但 LLM 并不会自动"看见"这些文档——只有当检索结果与用户问题被正确地组装进提示词(Prompt)后,LLM 才能生成基于事实的答案。读完本文你将掌握:为什么要把提示词拆成固定不变的 Instructions 与每次变化的 User Prompt 两部分、如何用build_context把检索结果格式化成 LLM 易读的文本块、如何用模板把问题与上下文组合成最终请求,以及这些代码在仓库 rag_helper.py 中如何被封装为可复用的类方法。
为什么必须构建提示词:LLM 看不到你的文档
在前一课 Search 中,我们用 minsearch 建立了索引并检索出了 5 条最相关的 FAQ 条目,得到了形如[{"section": ..., "question": ..., "answer": ...}]的字典列表。但这里有一个关键事实:LLM 是一个无状态的黑盒——它不知道你检索到了什么,除非你把内容写进发给它的文本里。
正如课程 What is RAG 中所演示的,直接把学生问题"I just discovered the course. Can I join now?"丢给模型,它只会给出"你可以通常加入、请查看课程官网"这类泛泛而谈的回答,因为它不了解 Zoomcamp 具体的报名与证书政策。RAG 的整个思路就是:在提问时把相关知识库文档检索出来,与问题一起放进提示词交给 LLM。提示词就是检索(Search)与生成(Generation)之间的桥梁。
因此,构建提示词需要解决两件事:
- 把用户的问题放进去;
- 把检索回来的文档放进去,并格式化成 LLM 容易理解的结构。
Prompt 的两段式结构:Instructions 与 User Prompt
当我们构建 AI 系统时,通常把提示词拆成两部分:
| 组成部分 | 别名 | 内容 | 变化频率 |
|---|---|---|---|
| Instructions | 系统提示(system prompt) | 告诉 LLM 扮演什么角色、如何作答 | 固定不变,每个请求都一样 |
| User prompt | 用户提示 | 承载实际问题与检索到的上下文 | 每次请求都不同 |
拆分的理由很朴素:Instructions 是固定的,User prompt 是变化的。分开存放让固定部分易于复用(写一次、处处生效),而变化部分则可以在每次请求时重新构建。这样既避免了重复维护,也让调试时能一眼看出"规则"与"数据"的边界。
这个设计在后续课程中进一步落地为两个独立的文件:ingest.py 负责数据加载与索引构建,rag_helper.py 负责 RAG 逻辑(搜索、提示词、LLM 调用),详见 RAG Helper。
Instructions:告诉 LLM 它的角色与边界
课程给出了如下 Instructions 模板:
INSTRUCTIONS = """ Your task is to answer questions from the course participants based on the provided context. Use the context to find relevant information and provide accurate answers. If the answer is not found in the context, respond with "I don't know." """这段提示做了三件事:
- 定义角色:你是回答课程学员问题的助手,而不是一个万能的百科;
- 定义信息源:答案必须基于提供的 context 得出;
- 定义失败行为:如果 context 中没有答案,就如实回复 "I don't know.",而不是编造。
最后一条至关重要——它正是把答案"锚定"在我们的数据上、降低幻觉(hallucination)的关键约束。这也是对 What is RAG 中"朴素 RAG"示例的正式化:当时我们手工把 FAQ 内容粘贴进 prompt,现在 Instructions 把这一行为规范成了可复用的固定规则。
仓库 rag_helper.py 中的INSTRUCTIONS与课程模板完全一致,并作为RAGBase构造函数的默认参数使用。
User Prompt 模板:为问题与上下文留出占位符
User prompt 部分使用模板字符串,用占位符接收每次请求都会变化的内容:
USER_PROMPT_TEMPLATE = """ Question: {question} Context: {context} """模板本身只是一个空壳,{question}和{context}两个占位符会在运行时被填充。注意观察它与 Instructions 的差异:这里没有任何"行为规则",只有数据布局——问题在上、上下文在下。
值得一提的是,仓库 rag_helper.py 中最终使用的PROMPT_TEMPLATE在措辞上略有演进(大写QUESTION:与CONTEXT:标签,并用.strip()去掉首尾空白),但结构完全一致。这恰好说明:提示词模板本身就是一个可以随意实验、按需调整的参数,课程把它的默认值固化在代码里,同时允许通过构造函数覆盖。
构建 Context:把检索结果格式化成文本
检索返回的是字典列表,而 LLM 只接受字符串。build_context就是二者之间的"序列化器":
def build_context(search_results): lines = [] for doc in search_results: lines.append(doc["section"]) lines.append("Q: " + doc["question"]) lines.append("A: " + doc["answer"]) lines.append("") return "\n".join(lines).strip()对每一条 FAQ 文档,它输出一个由三行组成的块:
- 第一行:所属 section(如 "General Course-Related Questions"),起到分组标题的作用;
Q:前缀的问题;A:前缀的答案;- 末尾追加一个空行,作为块与块之间的视觉分隔。
最后用"\n".join(lines)把所有行拼成一个字符串,再.strip()去掉首尾多余空白。这一步虽然简单,却是 RAG 管线中不可省略的预处理:它把结构化的列表数据,转换成了 LLM 最易读的"分块 + 标签"线性文本。上下文格式的好坏,直接影响 LLM 能否快速定位到答案。
课程配套截图(见文首)展示了build_context的典型输出:每个 FAQ 块以 section 开头,接着是Q:/A:成对的问题与答案,多块之间以空行分隔,排版清晰。
在 rag_helper.py 中,同样的逻辑被封装为RAGBase.build_context方法,逐行实现与这里完全一致。
构建完整 Prompt:问题与上下文的组装
有了模板和上下文,build_prompt只需把两者填进占位符:
def build_prompt(question, search_results): context = build_context(search_results) prompt = USER_PROMPT_TEMPLATE.format( question=question, context=context ) return prompt.strip()流程是:先调用build_context得到格式化后的上下文字符串,再用str.format分别填充模板中的{question}与{context},最后.strip()去掉可能的前后空白。返回的就是将直接发送给 LLM 的完整 User prompt。
在 rag_helper.py 中,RAGBase.build_prompt(query, search_results)的封装如出一辙,唯一区别是它使用实例属性self.prompt_template,从而允许为不同场景替换模板。
运行验证:打印出来的 Prompt 长什么样
在 notebook 中调用:
prompt = build_prompt(question, search_results) print(prompt)你应该看到问题在最上方、若干条 FAQ 条目紧随其后的完整提示词,大致如下:
Question: I just discovered the course. Can I join now? Context: General Course-Related Questions Q: I just discovered the course. Can I still join? A: Yes, but if you want to receive a certificate, you need to submit your project while we're still accepting submissions. General Course-Related Questions Q: Course: I have registered for the LLM Zoomcamp. When can I expect to receive the confirmation email? A: You don't need it. You're accepted. You can also just start learning and submitting homework... ...这就是我们最终发送给 LLM 的内容。注意两个细节:
- 问题来自用户,每次请求都不同;
- 上下文来自上一课的搜索步骤(默认
num_results=5,并带boost_dict={"question": 2.0, "section": 0.5}与filter_dict={"course": "llm-zoomcamp"}的检索配置,参见 05-search.md),而 FAQ 条目的question/answer/section字段结构来自 04-dataset.md 中抓取的 DataTalks.Club FAQ 数据。
这里省略了结尾的Answer:提示。正如 03-rag.md 中提到的:早期 GPT-3 这类模型需要这种补全提示来引导输出,而现代模型已经不需要这个"提示拐杖"。
消息历史:Instructions 与 User prompt 的正式分离
build_prompt组装出的是 User prompt 部分。但在真正调用 LLM 时,课程 The LLM 展示了更规范的做法:把整个请求组织成消息列表(message history),让固定指令与变化内容在"角色"层面就分开:
message_history = [ {"role": "developer", "content": INSTRUCTIONS}, {"role": "user", "content": prompt} ] response = openai_client.responses.create( model="gpt-5.4-mini", input=message_history )developer角色承载固定的 Instructions(系统级行为规则);user角色承载每次变化的 User prompt(问题 + 上下文)。
OpenAI 同时接受developer与system作为指令角色,课程统一使用developer。这样设计的好处是:即使提示词文本在物理上被拆成两块,它们在同一个请求中始终一起被发送,LLM 依然能"看到"完整的指令 + 数据。
从函数到类:仓库源码中的最终形态
课程中的独立函数(build_context、build_prompt)在 rag_helper.py 中被整合进RAGBase类。类方法与独立函数一一对应,并且把index、llm_client、instructions、prompt_template、course、model全部变成构造参数——这样任何 index(minsearch、sqlitesearch)和任何兼容 OpenAI 接口的客户端都可以即插即用:
class RAGBase: def __init__( self, index, llm_client, instructions=INSTRUCTIONS, prompt_template=PROMPT_TEMPLATE, course='llm-zoomcamp', model='gpt-5.4-mini' ): ...其中llm方法把本课的提示词构建与角色分离整合成一次完整调用(rag_helper.py):
def llm(self, prompt): input_messages = [ {'role': 'developer', 'content': self.instructions}, {'role': 'user', 'content': prompt} ] response = self.llm_client.responses.create( model=self.model, input=input_messages ) return response.output_text而rag方法则把search→build_prompt→llm三步串成完整管线(rag_helper.py)。从本课的"手工组装提示词"到这里的"类方法封装",演进脉络非常清晰:先理解每一行代码在做什么,再把它们沉淀成可复用组件。
自定义 Instructions:按需覆盖默认规则
由于instructions是构造参数,覆盖它就能改变模型行为,而不必改动任何管线代码。课程 RAG Helper 给出了一例:
custom_instructions = """ You're a course teaching assistant. Answer the QUESTION based on the CONTEXT from the FAQ database. Use only the facts from the CONTEXT when answering the QUESTION. """.strip() assistant = RAGBase( index=index, llm_client=openai_client, instructions=custom_instructions, )你可以自由实验:更强的约束("只能引用 CONTEXT 中的事实")、更具体的输出格式("先给结论再给依据")、或针对不同课程定制角色。提示词工程一半是艺术、一半是科学——你需要不断尝试不同的措辞、结构、字段标签,观察输出变化。
Prompt 质量与幻觉的关系
本课结尾给出了一个值得记住的论断:
The prompt is the bridge between search and the LLM. A bad prompt lets the LLM ignore the context and hallucinate. A good prompt keeps the answer grounded.
即:
- 坏提示词:上下文没有被清晰标记、指令含糊、未定义"不知道"的行为,LLM 就会忽略上下文去编造;
- 好提示词:用
Question:/Context:明确分隔数据、用 Instructions 约束信息源、用 "I don't know." 兜底,答案就会被牢牢锚定在检索结果上。
同时要诚实看待提示词工程的局限性:本课的模板是一个良好的起点,而不是终点。正如课程后续 Evaluation 模块所强调的,判断一个提示词好坏最终要靠评估指标(如命中率、MRR、LLM-as-judge)来度量,而不是靠感觉猜测。到那个阶段,你就能量化"调整提示词"到底带来了多少收益。
小结与下一步
回顾本课构建的完整提示词组装链路:
INSTRUCTIONS:固定不变的角色与行为约束,锚定答案、抑制幻觉;USER_PROMPT_TEMPLATE:为{question}与{context}预留占位符的固定骨架;build_context:把检索到的字典列表格式化为section+Q:/A:分块的易读文本;build_prompt:把问题与上下文填入模板,产出最终 User prompt;- 消息历史:在请求层把
developer指令与user数据正式分离; - 类封装:以上逻辑在 rag_helper.py 中固化为
RAGBase,可通过参数覆盖指令、模板、模型与索引。
至此,RAG 三大件中"提示词"这一环已经就位。下一步是 The LLM:把这个提示词真正发送给模型,读取response.output_text,并把搜索、提示词、LLM 三部分组装成完整的rag()函数——那将是整条 RAG 管线第一次端到端跑通。
【免费下载链接】llm-zoomcampLLM Zoomcamp - a free online course about real-life applications of LLMs. In 10 weeks you will learn how to build an AI system that answers questions about your knowledge base. Register here 👇🏼项目地址: https://gitcode.com/GitHub_Trending/ll/llm-zoomcamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考