Haystack Builders 组件实战:用 PromptBuilder、ChatPromptBuilder 与 AnswerBuilder 构建 RAG 提示与答案管线
【免费下载链接】haystackOpen-source AI orchestration framework for building context-engineered, production-ready LLM applications. Design modular pipelines and agent workflows with explicit control over retrieval, routing, memory, and generation. Built for scalable agents, RAG, multimodal applications, semantic search, and conversational systems.项目地址: https://gitcode.com/GitHub_Trending/ha/haystack
导读
在 Haystack 的组件体系中,builders模块承担着“提示词构造”与“答案后处理”两大职责,是任何 RAG(检索增强生成)或 Agent 流水线中都绕不开的拼图。本篇指南以 docs-website/reference/haystack-api/builders_api.md 为骨架,完整讲解PromptBuilder(文本提示渲染)、ChatPromptBuilder(对话提示渲染)与AnswerBuilder(答案与引用解析)三个组件的参数、用法和 Pipeline 集成方式,并结合 haystack/components/builders 目录下的源码实现与 test/components/builders 的测试用例做源码级佐证。读完本文,你将能够:把检索文档与用户查询渲染成符合 Generator 输入格式的提示词;在运行期动态更换模板、覆盖变量以做提示词工程;以及把 Generator 的原始输出解析为携带引用来源的GeneratedAnswer结构化答案。
一、Builders 模块全景
builders是 Haystack 中专门负责“提示构建”与“答案组装”的组件集合,位于 haystack/components/builders/init.py,对外暴露三个组件:
| 组件 | 职责 | 输出 |
|---|---|---|
PromptBuilder | 用 Jinja2 模板渲染纯文本提示词,供非 Chat 类 Generator 使用 | {"prompt": str} |
ChatPromptBuilder | 用 Jinja2 模板渲染一组ChatMessage对话消息,供 Chat 类 Generator 使用 | {"prompt": list[ChatMessage]} |
AnswerBuilder | 用正则从 Generator 回复中提取答案,并解析文档引用,组装成GeneratedAnswer | {"answers": list[GeneratedAnswer]} |
三者天然串联:PromptBuilder/ChatPromptBuilder负责“送进去”,AnswerBuilder负责“取出来”。下文按“先取后送”的顺序,从答案解析讲起,再回到提示构造。
二、AnswerBuilder:把 Generator 回复解析成结构化答案
2.1 组件定位与核心能力
AnswerBuilder位于 haystack/components/builders/answer_builder.py,用一句话概括:把“查询 + Generator 回复”转换为GeneratedAnswer对象。它的三个核心能力是:
- 正则提取答案:通过
pattern参数从 Generator 的原始回复中截取答案文本; - 引用解析:通过
reference_pattern参数解析回复中的[n]形式引用标记,把被引用的输入文档挂到答案上; - 兼容两类 Generator:
replies既可以传list[str](非 Chat Generator 输出),也可以传list[ChatMessage](Chat Generator 输出),源码在 answer_builder.py 中通过isinstance(reply, ChatMessage)统一处理。
GeneratedAnswer定义于 haystack/dataclasses/answer.py,是一个包含data(答案文本)、query(原始查询)、documents(引用文档列表)、meta(元数据)四个字段的 dataclass,支持to_dict/from_dict序列化。
2.2 最简用法:正则提取答案
原文档给出的最小示例:
from haystack.components.builders import AnswerBuilder builder = AnswerBuilder(pattern="Answer: (.*)") builder.run(query="What's the answer?", replies=["This is an argument. Answer: This is the answer."])这里pattern="Answer: (.*)"会从回复中匹配到捕获组This is the answer.。pattern参数的取值规则(见 answer_builder.py 与 answer_builder.py 的_extract_answer_string实现):
- 不传
pattern时,整个回复文本就是答案; - 正则最多允许一个捕获组:有捕获组时取
match.group(1),无捕获组时取match.group(0)即整个匹配; - 超过一个捕获组会在初始化或运行时抛出
ValueError(由_check_num_groups_in_regex校验,见 answer_builder.py),测试用例test_run_with_pattern_with_more_than_one_capturing_group验证了这一点; - 未匹配到时返回空字符串
""。
官方示例还给出另一个经典模式:[^\n]+$可在一串多行文本中取出最后一行的答案。
2.3 引用文档解析:带来源的答案
这是AnswerBuilder最有价值的能力。原文档的核心示例:
from haystack import Document from haystack.components.builders import AnswerBuilder replies = ["The capital of France is Paris [2]."] docs = [ Document(content="Berlin is the capital of Germany."), Document(content="Paris is the capital of France."), Document(content="Rome is the capital of Italy."), ] builder = AnswerBuilder(reference_pattern=r"\[(\d+)\]", return_only_referenced_documents=False) result = builder.run(query="What is the capital of France?", replies=replies, documents=docs)["answers"][0] print(f"Answer: {result.data}") print("References:") for doc in result.documents: if doc.meta["referenced"]: print(f"[{doc.meta['source_index']}] {doc.content}") print("Other sources:") for doc in result.documents: if not doc.meta["referenced"]: print(f"[{doc.meta['source_index']}] {doc.content}") # >> Answer: The capital of France is Paris # >> References: # >> [2] Paris is the capital of France. # >> Other sources: # >> [1] Berlin is the capital of Germany. # >> [3] Rome is the capital of Italy.运行结果中每个返回文档的meta都会被打上两个键(详见 answer_builder.py):
source_index:该文档在输入documents列表中的 1-based 位置(即第 1 个文档为 1,第 2 个为 2);referenced:布尔值,表示该文档是否被回复中的引用标记命中。
reference_pattern的语义要点:
- 引用从 [1] 开始计数,对应输入文档列表的第一个元素;回复中出现
[2]即引用第二个文档; - 不提供
reference_pattern时不做引用解析,全部文档原样返回,也不会有referenced键; - 输入文档不会被修改:源码使用
dataclasses.replace(doc, meta=doc_meta)生成带新元数据的副本(answer_builder.py),测试用例test_run_does_not_mutate_input_documents_meta与test_run_does_not_mutate_document_with_empty_meta专门守护了这一行为; - 越界引用会被跳过并记录 WARNING 日志(如
[0]、[3]但只有 2 个文档),且对[0]做了显式边界检查,避免 Python 负索引静默解析到最后一个文档(见 answer_builder.py 及测试test_run_with_documents_with_zero_reference)。
2.4return_only_referenced_documents与引用范围展开
__init__签名(answer_builder.py):
__init__( pattern: str | None = None, reference_pattern: str | None = None, last_message_only: bool = False, *, return_only_referenced_documents: bool = True, expand_reference_ranges: bool = False ) -> None各参数行为:
return_only_referenced_documents:默认True,只返回被引用的文档;设为False则返回全部文档,但每个文档仍带referenced标记(供上层区分“被引用”与“其他来源”)。未提供reference_pattern时该参数无效果;expand_reference_ranges:默认False(保持向后兼容)。为True时会把[6-10]这类范围标记展开为第 6 到第 10 个文档。源码层面,当它与默认引用模式\[(\d+)\]配合时,会自动切换为更宽的模式\[(\d+(?:[,-]\d+)*)\](见 answer_builder.py 与_resolve_reference_pattern),支持逗号分隔的混合写法如[1-3,7-9];last_message_only:默认False,对replies中每条消息各生成一个GeneratedAnswer;为True时只取最后一条消息生成答案(answer_builder.py),测试用例test_conversation_history_with_last_message_only_true展示了多轮对话场景。
关于引用范围展开的边界处理,测试用例给出了可验证的细节:[3-1]这类 start>end 的非法区间会被忽略(test_run_ignores_invalid_reference_ranges);[1-100]超出文档数时会把终点钳制到文档总数,避免一次性物化巨大集合(test_run_clamps_reference_range_to_number_of_documents,对应 answer_builder.py 的 clamp 逻辑)。
2.5run方法签名与元数据合并
run签名(answer_builder.py):
run( query: str, replies: list[str] | list[ChatMessage], meta: list[dict[str, Any]] | None = None, documents: list[Document] | None = None, pattern: str | None = None, reference_pattern: str | None = None, expand_reference_ranges: bool | None = None, ) -> dict[str, Any]注意pattern、reference_pattern、expand_reference_ranges三个参数既可在__init__设置,也可在run时覆盖(run中的值优先,见 answer_builder.py),这为“同一个组件在不同 Pipeline 运行中采用不同解析规则”提供了灵活性。
meta参数的行为值得留意:不传时默认按replies数量填充空字典;若传入则长度必须与replies一致,否则抛ValueError(answer_builder.py)。对于ChatMessage类型的回复,其自带meta会与传入meta合并({**extracted_metadata, **given_metadata}),且答案的meta中始终会写入all_messages键保存完整对话历史(answer_builder.py)。
2.6 源码佐证:测试覆盖的行为契约
test/components/builders/test_answer_builder.py 对上述行为做了系统验证,可作为读者理解组件语义的“行为说明书”:
test_run_without_pattern/test_run_with_pattern_with_capturing_group:验证无模式时整段回复即答案、有捕获组时取捕获组内容;test_run_with_documents_with_reference_pattern:验证[2]引用只返回第 2 个文档,且meta["referenced"]、meta["source_index"]正确;test_run_returns_referenced_documents_in_source_order:引用结果按输入文档顺序(升序)返回;test_run_with_chat_message_replies_with_pattern:验证ChatMessage输入路径与元数据透传。
三、PromptBuilder:Jinja2 渲染纯文本提示词
3.1 组件定位
PromptBuilder(haystack/components/builders/prompt_builder.py)使用 Jinja2 语法渲染提示词模板,输出可直接发送给 Generator 的字符串。模板中的变量默认全部必填,可通过required_variables放行部分变量为可选(缺失时渲染为空字符串)。
3.2 独立运行的最小示例
from haystack.components.builders import PromptBuilder template = "Translate the following context to {{ target_language }}. Context: {{ snippet }}; Translation:" builder = PromptBuilder(template=template) builder.run(target_language="spanish", snippet="I can't speak spanish.")渲染结果为:"Translate the following context to Spanish. Context: I can't speak Spanish.; Translation:"。
3.3 在 RAG Pipeline 中使用
原文档的经典 RAG 示例,完整展示了检索结果与查询如何注入提示词并喂给 Chat Generator:
from haystack import Pipeline, Document from haystack.utils import Secret from haystack.components.generators.chat import OpenAIChatGenerator from haystack.components.builders.prompt_builder import PromptBuilder # in a real world use case documents could come from a retriever, web, or any other source documents = [Document(content="Joe lives in Berlin"), Document(content="Joe is a software engineer")] prompt_template = """ Given these documents, answer the question. Documents: {% for doc in documents %} {{ doc.content }} {% endfor %} Question: {{query}} Answer: """ p = Pipeline() p.add_component(instance=PromptBuilder(template=prompt_template), name="prompt_builder") p.add_component(instance=OpenAIChatGenerator(api_key=Secret.from_env_var("OPENAI_API_KEY")), name="llm") p.connect("prompt_builder", "llm") question = "Where does Joe live?" result = p.run({"prompt_builder": {"documents": documents, "query": question}}) print(result)这里的p.connect("prompt_builder", "llm")把PromptBuilder的prompt输出接到 Chat Generator 的messages输入。由于模板使用了{% for doc in documents %}循环与doc.content属性访问,说明 Jinja2 模板中可以直接遍历Document对象并访问其字段。
3.4 运行时更换模板与覆盖变量(提示词工程)
PromptBuilder一个重要的实战特性是:无需重建 Pipeline,即可在每次run时更换模板或覆盖变量。
更换模板:把新模板字符串通过template参数传入:
new_template = """ You are a helpful assistant. Given these documents, answer the question. Documents: {% for doc in documents %} Document {{ loop.index }}: Document name: {{ doc.meta['name'] }} {{ doc.content }} {% endfor %} Question: {{ query }} Answer: """ p.run({ "prompt_builder": { "documents": documents, "query": question, "template": new_template, }, })该模板展示了loop.index(循环序号)与doc.meta['name'](文档元数据)的访问方式。源码层面,run在收到template参数时会用self._env.from_string(template)现场编译新模板再渲染(prompt_builder.py)。
覆盖变量:用template_variables覆盖 Pipeline 变量,或引入模板中新出现但未绑定 Pipeline 输入的变量:
language_template = """ ... Question: {{ query }} Please provide your answer in {{ answer_language | default('English') }} Answer: """ p.run({ "prompt_builder": { "documents": documents, "query": question, "template": language_template, "template_variables": {"answer_language": "German"}, }, })这里answer_language不在 Pipeline 输入中,模板里用default('English')兜底;运行时通过template_variables覆盖为"German"。run实现中template_variables与 kwargs 会合并({**kwargs, **template_variables}),后者优先级更高(prompt_builder.py)。
3.5__init__参数详解
__init__( template: str, required_variables: list[str] | Literal["*"] | None = "*", variables: list[str] | None = None, ) -> Nonetemplate(必填):Jinja2 模板字符串,如"Summarize this document: {{ documents[0].content }}\nSummary:"。模板中出现的变量会被自动识别为组件输入;required_variables:默认"*",表示模板中所有变量必填;传显式列表则只要求列出的变量必填,其余变量缺失时渲染为空字符串;传None表示全部可选。注意:当存在模板变量且显式设置None时,源码会打印警告日志(prompt_builder.py),提示在多分支 Pipeline 中“全可选”可能导致意外行为;variables:显式指定输入变量列表,替代从模板自动推断的结果。典型场景是提示词工程期:模板里暂时用不到的变量也想作为输入预留,可以在此列出。源码通过_extract_template_variables_and_assignments(haystack/utils/jinja2_extensions.py)从模板提取“用到的变量”并减去“模板内已赋值的变量”。
组件初始化时会对每个变量调用component.set_input_type:必填变量类型为Any,可选变量类型为Any且带默认值""(prompt_builder.py),这解释了“可选变量缺失时渲染为空字符串”的机制来源。
3.6run方法与校验
run( template: str | None = None, template_variables: dict[str, Any] | None = None, **kwargs: Any ) -> dict[str, Any]返回值恒为{"prompt": str}。_validate_variables(prompt_builder.py)在渲染前校验必填变量是否齐备,缺失时抛出带完整信息的ValueError(列出缺失变量、必填列表与已提供列表),这正是run文档中Raises ValueError的来源。
四、ChatPromptBuilder:渲染多轮对话消息
4.1 组件定位
ChatPromptBuilder(haystack/components/builders/chat_prompt_builder.py)是PromptBuilder的对话版:模板可以是list[ChatMessage],也可以是带{% message %}块的特殊字符串;渲染结果是一组ChatMessage,直接对接 Chat Generator 的messages输入。
4.2 静态 ChatMessage 模板
from haystack.dataclasses import ChatMessage from haystack.components.builders import ChatPromptBuilder template = [ChatMessage.from_user("Translate to {{ target_language }}. Context: {{ snippet }}; Translation:")] builder = ChatPromptBuilder(template=template) builder.run(target_language="spanish", snippet="I can't speak spanish.")模板变量同样用{{ }}表示,且user/system 角色的消息会被渲染,其他角色的消息(如历史 assistant 消息)原样保留(chat_prompt_builder.py)。
4.3 运行时覆盖静态模板
msg = "Translate to {{ target_language }} and summarize. Context: {{ snippet }}; Summary:" summary_template = [ChatMessage.from_user(msg)] builder.run(target_language="spanish", snippet="I can't speak spanish.", template=summary_template)与PromptBuilder一样,run的template参数可以在每次运行覆盖默认模板。
4.4 动态模板 + Pipeline 集成
更常见的做法是不在初始化时绑定模板,而在每次run时传入(原文档中的完整示例):
from haystack.components.builders import ChatPromptBuilder from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage from haystack import Pipeline # no parameter init, we don't use any runtime template variables prompt_builder = ChatPromptBuilder() llm = OpenAIChatGenerator(model="gpt-5-mini") pipe = Pipeline() pipe.add_component("prompt_builder", prompt_builder) pipe.add_component("llm", llm) pipe.connect("prompt_builder.prompt", "llm.messages") location = "Berlin" language = "English" system_message = ChatMessage.from_system("You are an assistant giving information to tourists in {{language}}") messages = [system_message, ChatMessage.from_user("Tell me about {{location}}")] res = pipe.run(data={"prompt_builder": {"template_variables": {"location": location, "language": language}, "template": messages}})这里有两个值得注意的 Pipeline 用法:
ChatPromptBuilder()不传模板直接初始化,模板与变量全部通过run(Pipeline 的data字典)下发;template_variables与template同时在 Pipeline 输入中提供——前者放变量、后者放消息模板。第二次运行时,模板被替换为包含{{day_count}}的新消息,template_variables提供day_count等变量,同一个 Pipeline 就完成了两轮不同的对话构建。
4.5 字符串模板与多模态内容
ChatPromptBuilder还支持用特殊字符串模板一次性声明多个消息,配合{% message role="..." %}块与templatize_part过滤器,可在消息中嵌入图片等多模态内容:
from haystack.components.builders import ChatPromptBuilder from haystack.dataclasses.image_content import ImageContent template = """ {% message role="system" %} You are a helpful assistant. {% endmessage %} {% message role="user" %} Hello! I am {{user_name}}. What's the difference between the following images? {% for image in images %} {{ image | templatize_part }} {% endfor %} {% endmessage %} """ images = [ImageContent.from_file_path("test/test_files/images/apple.jpg"), ImageContent.from_file_path("test/test_files/images/haystack-logo.png")] builder = ChatPromptBuilder(template=template) builder.run(user_name="John", images=images)实现上,字符串模板由ChatMessageExtension(Jinja2 扩展,定义于 haystack/utils/jinja2_chat_extension.py)处理:渲染后按行切分、每行是一段 JSON,再由ChatMessage.from_dict还原为消息对象(chat_prompt_builder.py)。templatize_part过滤器(同一文件 jinja2_chat_extension.py)负责把ImageContent等结构化内容安全地序列化为消息的一部分,并且只能在字符串模板中使用——若在list[ChatMessage]模板中出现会抛出FILTER_NOT_ALLOWED_ERROR_MESSAGE(chat_prompt_builder.py)。注意示例中图片路径是测试用相对路径,实际使用时请替换为你自己的图片路径。
4.6__init__与run签名
__init__( template: list[ChatMessage] | str | None = None, required_variables: list[str] | Literal["*"] | None = "*", variables: list[str] | None = None, ) -> None run( template: list[ChatMessage] | str | None = None, template_variables: dict[str, Any] | None = None, **kwargs: Any ) -> dict[str, list[ChatMessage]]与PromptBuilder几乎一致的参数语义,差异在于:
- 模板类型为
list[ChatMessage] | str | None(可不在初始化时提供); - 变量提取只针对user 与 system 角色的消息(chat_prompt_builder.py);
run返回{"prompt": list[ChatMessage]};run中若模板为空、或列表中含有非ChatMessage元素,会抛出ValueError(chat_prompt_builder.py);- 渲染后的消息通过
dataclasses.replace生成新对象,不会原地修改传入的模板消息(chat_prompt_builder.py)。
4.7 序列化支持
ChatPromptBuilder实现了to_dict/from_dict(chat_prompt_builder.py):to_dict会把list[ChatMessage]模板转成字典列表;from_dict反序列化时再还原为ChatMessage对象。这使组件可以被 Pipeline 的 YAML/JSON 序列化机制持久化。PromptBuilder同样提供to_dict(prompt_builder.py),保存的是原始模板字符串。
五、渲染环境的底层实现细节
两个 Builder 组件的模板渲染并不是裸用 Jinja2,而是统一构建在一个沙箱环境之上,了解这层实现有助于理解安全边界与能力边界:
- 沙箱环境:
PromptBuilder与ChatPromptBuilder都使用HaystackSandboxedEnvironment(继承自 Jinja2 的SandboxedEnvironment,见 haystack/utils/jinja2_sandbox.py),对模板中可访问的对象与方法做了白名单式限制,避免模板执行任意 Python 代码; - 时间扩展:若安装了可选依赖
arrow(pip install "arrow>=1.3.0"),环境会额外注册Jinja2TimeExtension,允许模板中使用当前时间相关能力;PromptBuilder在导入失败时降级为纯沙箱环境(prompt_builder.py),ChatPromptBuilder则通过LazyImport延迟导入(chat_prompt_builder.py); - 变量推断:
_extract_template_variables_and_assignments(haystack/utils/jinja2_extensions.py)负责从模板文本中区分“被使用的变量”与“模板内部已赋值(如{% set %})的变量”,只把前者暴露为组件输入。
六、三个组件的选型与串联建议
| 场景 | 推荐组件 | 理由 |
|---|---|---|
纯文本提示、非 Chat Generator(如OpenAIGenerator) | PromptBuilder | 输出str,与生成器输入类型直接匹配 |
多轮对话、Chat Generator(如OpenAIChatGenerator) | ChatPromptBuilder | 输出list[ChatMessage],保留角色与多模态内容 |
| 需要给答案带引用来源(RAG 引用标注) | AnswerBuilder | 正则解析答案 +[n]引用还原文档,产出GeneratedAnswer |
一个典型的端到端 RAG 链路是:Retriever → PromptBuilder/ChatPromptBuilder → Generator → AnswerBuilder。其中AnswerBuilder的documents输入直接接 Retriever 的documents输出,replies接 Generator 的replies输出,即可在最终答案中同时拿到答案文本、被引用的原文片段与元数据。完整的示例可在builders组件源码 docstring(answer_builder.py、prompt_builder.py)与官方示例目录 examples 中继续探索。
七、小结
本文围绕 Haystackbuilders模块的三个组件展开了完整梳理:
PromptBuilder:Jinja2 纯文本提示渲染,支持required_variables部分可选、variables显式声明输入、运行时template更换与template_variables变量覆盖;ChatPromptBuilder:对话消息渲染,支持list[ChatMessage]与字符串两种模板形态,可嵌入templatize_part多模态内容,渲染结果直接对接 Chat Generator;AnswerBuilder:正则提取答案、解析[n]引用还原文档、合并元数据,产出携带source_index与referenced标记的GeneratedAnswer。
三者共同的底层支撑是 Jinja2 沙箱渲染环境(haystack/utils/jinja2_sandbox.py)与变量提取工具(haystack/utils/jinja2_extensions.py)。行为契约则由 test/components/builders 下的三个测试文件完整守护,读者若想深挖边界行为(越界引用、范围展开、消息不可变性等),直接阅读对应测试是最快的路径。
【免费下载链接】haystackOpen-source AI orchestration framework for building context-engineered, production-ready LLM applications. Design modular pipelines and agent workflows with explicit control over retrieval, routing, memory, and generation. Built for scalable agents, RAG, multimodal applications, semantic search, and conversational systems.项目地址: https://gitcode.com/GitHub_Trending/ha/haystack
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考