跳转到内容

自定义 Synthesizer 与 Prompt 工程

前两节我们学习了响应合成的挑战和四种内置模式。但内置模式的 Prompt 模板是通用的——它们不知道你的应用是法律咨询还是技术支持,不知道用户偏好简洁回答还是详细解释,也不知道答案应该用什么语言风格。

这一节我们来学习如何通过自定义 Synthesizer 和 Prompt 工程来精确控制合成的行为和输出质量。这是让 RAG 系统从"能用"到"好用"的关键一步。

为什么需要自定义 Prompt?

让我们看看 LlamaIndex 默认的 REFINE 模式使用的 Prompt(简化版):

System Prompt:
你是一个有帮助的助手,请根据上下文信息回答问题。
如果你不知道答案,就说不知道。

Initial Query Prompt:
上下文信息:
{context_str}

问题:
{query_str}

Refine Prompt:
我们有一个关于'{query_str}'的现有答案:
'{existing_answer}'
以下是新的参考信息(可能有助于改进答案):
'{context_msg}'
如果新信息有用,请精炼原有答案;如果没有用,保持不变。

这个默认 Prompt 的问题在于:

问题一:缺少领域约束。 它没有告诉 LLM "你是法律顾问"或"你是技术支持工程师",导致回答风格可能不符合业务场景的需求。

问题二:没有格式要求。 它没有要求输出 Markdown 表格、JSON 结构或特定的分段方式。

问题三:没有安全边界。 它没有明确禁止某些行为(如编造文档中没有的信息、泄露内部系统细节等)。

问题四:没有引用规范。 它没有要求在答案中标注信息来源,导致用户无法追溯。

通过自定义 Prompt,我们可以解决所有这些问题。

基础自定义:替换 Prompt 模板

python
from llama_index.core import VectorStoreIndex, Settings
from llama_index.core.prompts.prompt_template import PromptTemplate
from llama_index.core.response_synthesizers import get_response_synthesizer
from llama_index.core.response_synthesizers import ResponseMode

index = VectorStoreIndex.from_documents(documents)

text_qa_template = PromptTemplate(
    "你是公司智能客服助手'小智',专门负责解答客户关于产品和服务的问题。\n\n"
    "【重要规则】\n"
    "1. 只使用以下参考资料中的信息来回答问题,不要编造任何内容\n"
    "2. 如果参考资料中没有相关信息,请诚实告知并建议联系人工客服\n"
    "3. 回答时请在适当位置标注来源,格式为 (来源: 文件名)\n"
    "4. 使用友好、专业的语气,避免过于口语化或过于正式\n"
    "5. 如果涉及操作步骤,请用编号列表清晰列出\n\n"
    "参考资料:\n{context_str}\n\n"
    "客户问题:\n{query_str}\n"
)

refine_template = PromptTemplate(
    "我们有一个关于'{query_str}'的现有回答:\n"
    "'{existing_answer}'\n\n"
    "以下是从知识库中检索到的新的参考信息:\n"
    "'{context_msg}'\n\n"
    "请基于新信息精炼原有回答。具体要求:\n"
    "- 如果新信息与已有内容矛盾,以新信息为准并注明变化\n"
    "- 如果新信息补充了有用细节,将其自然地融入回答中\n"
    "- 保持原有的友好语气和格式规范\n"
    "- 更新来源标注(如有)\n"
    "- 如果新信息没有帮助,保持原样不变\n"
)

synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.REFINE,
    text_qa_template=text_qa_template,
    refine_template=refine_template,
)

query_engine = index.as_query_engine(response_synthesizer=synthesizer)
response = query_engine.query("怎么申请退款?")
print(response.response)

自定义前后的效果对比

使用默认 Prompt 的输出:

根据文档,您可以登录账户进入订单管理页面,
点击申请退款按钮。退款将在3-5个工作日内退回。

使用自定义 Prompt 的输出:

您好!关于退款申请的问题,我为您整理了以下信息 (来源: FAQ.md):

📋 **退款条件**
• 支持购买后30天内无理由退款 (来源: refund_policy.pdf)
• 定制类产品(如刻字、特殊配色)不适用此政策 (来源: refund_policy.pdf)

🔄 **操作步骤** (来源: user_guide.md)
1. 登录您的账户 → 进入「我的订单」页面
2. 找到需要退款的订单 → 点击「申请退款」按钮
3. 选择退款原因(必填)→ 提交审核
4. 审核通过后 3-5 个工作日内原路退回

💡 **温馨提示**
如果您购买的是定制版产品或有其他特殊情况,
建议先联系人工客服(400-xxx-xxxx)确认是否适用无理由退款政策。

差异是显而易见的——自定义 Prompt 让输出从"一段平铺直叙的文字"变成了"结构清晰、有来源标注、有温馨提示的专业回复"。

高级技巧:条件化 Prompt

有时候你想根据查询类型动态调整 Prompt 策略:

python
from typing import List
from llama_index.core.schema import TextNode


class AdaptivePromptSynthesizer(BaseResponseSynthesizer):
    """根据查询特征自适应选择 Prompt 的 Synthesizer"""

    def __init__(self, llm=None, **kwargs):
        super().__init__(**kwargs)
        self.llm = llm or Settings.llm

        self.templates = {
            "factual": PromptTemplate(
                "请严格基于资料回答,给出精确的事实性答案。\n\n"
                "资料:{context_str}\n问题:{query_str}"
            ),
            "procedural": PromptTemplate(
                "请按步骤详细说明操作方法。\n\n"
                "资料:{context_str}\n问题:{query_str}"
            ),
            "comparative": PromptTemplate(
                "请从多个角度比较分析。\n\n"
                "资料:{context_str}\n问题:{query_str}"
            ),
        }

    def _classify_query(self, query: str) -> str:
        """简单分类查询类型"""
        factual_keywords = ["多少", "什么", "是否", "有没有", "价格", "时间"]
        procedural_keywords = ["怎么", "如何", "步骤", "流程", "操作"]
        comparative_keywords = ["比较", "区别", "差异", "哪个好", "优缺点"]

        if any(k in query for k in factual_keywords):
            return "factual"
        elif any(k in query for k in procedural_keywords):
            return "procedural"
        elif any(k in query for k in comparative_keywords):
            return "comparative"
        return "factual"  # 默认

    def synthesize(self, query, nodes, **kwargs):
        qtype = self._classify_query(query)
        template = self.templates[qtype]

        context = "\n\n".join([f"[{i+1}] {n.text}" for i, n in enumerate(nodes)])
        prompt = template.format(context_str=context, query_str=query)

        response_text = self.llm.complete(prompt).text

        return Response(
            response=response_text,
            source_nodes=[NodeWithScore(node=n) for n in nodes],
            metadata={"query_type": qtype},
        )

这种"先判断再选择"的模式在生产环境中非常实用——不同类型的查询确实需要不同的处理策略。

多阶段合成管道

对于特别复杂的合成需求,你可以构建一个多阶段的处理管道:

python
class MultiStageSynthesizer(BaseResponseSynthesizer):
    """多阶段合成:草稿 → 审核 → 优化 → 格式化"""

    def __init__(self, llm=None, **kwargs):
        super().__init__(**kwargs)
        self.llm = llm or Settings.llm

    def synthesize(self, query, nodes, **kwargs):
        context = self._build_context(nodes)

        # Stage 1: 草稿生成
        draft_prompt = (
            f"基于以下资料生成一份完整的回答草稿。\n"
            f"不需要完美,但尽量覆盖所有关键点。\n\n"
            f"资料:\n{context}\n\n问题: {query}"
        )
        draft = self.llm.complete(draft_prompt).text
        print(f"[Stage 1] 草稿: {len(draft)} 字符")

        # Stage 2: 自检与修正
        review_prompt = (
            f"以下是针对问题'{query}'的一份回答草稿:\n{draft}\n\n"
            f"原始参考资料:\n{context}\n\n"
            f"请审核这份草稿:\n"
            f"1. 是否所有事实都来自参考资料?(标记任何编造的内容)\n"
            f"2. 是否遗漏了重要信息?\n"
            f"3. 是否有逻辑矛盾或不一致之处?\n"
            f"4. 输出修改后的版本,并附上修改说明。"
        )
        reviewed = self.llm.complete(review_prompt).text
        print(f"[Stage 2] 审核完成: {len(reviewed)} 字符")

        # Stage 3: 格式化输出
        format_prompt = (
            f"将以下回答转换为最终输出格式。\n\n"
            f"{reviewed}\n\n"
            f"输出要求:\n"
            f"- 使用 Markdown 格式\n"
            f"- 关键信息用加粗标注\n"
            f"- 操作步骤用有序列表\n"
            f"- 在每段重要信息后标注(来源: 文件名)\n"
            f"- 开头给出一句简短总结"
        )
        final = self.llm.complete(format_prompt).text
        print(f"[Stage 3] 最终版本: {len(final)} 字符")

        return Response(
            response=final,
            source_nodes=[NodeWithScore(node=n) for n in nodes],
            metadata={"stages": 3},
        )

这个三阶段管道虽然成本较高(3 次 LLM 调用),但对于高质量要求的场景(如法律意见书、医疗建议、金融报告等)能显著提升输出质量。

领域适配示例:不同行业的 Prompt

法律行业

python
legal_template = PromptTemplate(
    "您是一位资深法律顾问。请基于提供的法规和政策文件回答法律咨询。\n\n"
    "【职业准则】\n"
    "- 仅依据提供的法律法规文本进行分析\n"
    "- 明确区分'法律规定'和'实务建议'(后者应标注'仅供参考')\n"
    "- 引用时注明具体的法条名称和条款号\n"
    "- 对于模糊或存在争议的法律问题,说明不同观点\n"
    "- 如资料不足以得出结论,明确说明并建议咨询专业律师\n\n"
    "法规资料:\n{context_str}\n\n"
    "法律咨询:\n{query_str}"
)

技术支持

python
tech_support_template = PromptTemplate(
    "你是公司的技术支持工程师。帮助用户解决产品使用中的技术问题。\n\n"
    "【回答规范】\n"
    "- 先确认用户的产品型号和版本号\n"
    "- 操作步骤要具体到菜单路径和按钮名称\n"
    "- 给出命令行/代码示例时要完整可运行\n"
    "- 区分'正常现象'和'故障现象'\n"
    "- 提供排查思路而非直接给答案(引导用户自主解决)\n"
    "- 无法解决时提供升级渠道(工单/电话)\n\n"
    "技术文档:\n{context_str}\n\n"
    "用户问题:\n{query_str}"
)

医疗健康

python
healthcare_template = PromptTemplate(
    "您是一位医疗健康顾问助手。请基于提供的医学资料回答健康相关问题。\n\n"
    "【⚠️ 重要免责声明】\n"
    "- 以下回答不能替代专业医生的诊断和治疗建议\n"
    "- 如涉及症状描述,请强调'请及时就医' \n"
    "- 不提供处方药推荐或剂量建议\n"
    "- 引用的医学信息应标注数据来源和发布日期\n"
    "- 对紧急情况提供急救指导并强烈建议拨打急救电话\n\n"
    "医学参考资料:\n{context_str}\n\n"
    "健康咨询:\n{query_str}"
)

常见误区

误区一:"Prompt 越长越好。" 不是的。过长的 Prompt 会:占用宝贵的上下文窗口空间、增加 LLM 处理时间、可能引入相互矛盾的指令(指令越多越容易冲突)。好的 Prompt 应该精炼且聚焦——每个指令都应该有明确的目的。

误区二:"Prompt 写好后就不需要改了。" Prompt 的效果会随着 LLM 模型的更新而变化(新模型可能对不同的指令格式更敏感),也会随着用户反馈的积累而发现可优化的点。把 Prompt 当作需要持续迭代的代码来管理,而不是一次性写完就封存。

误区三:"自定义 Synthesizer 很难实现。" 从上一节的代码框架可以看到,最简单的自定义只需要实现 synthesize() 一个方法。真正复杂的是设计好的 Prompt 和处理逻辑——但这更多是领域知识和写作能力的问题,不是编程难题。

基于 MIT 许可发布