chat_prompts.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. from typing import List, Dict, Optional, Any
  2. def map_prompt(question: str, format_context: str) -> str:
  3. prompt = (
  4. f"【任务说明】:针对下方单个文档片段,提炼与“问题”直接相关的‘可被引用的事实要点’\n"
  5. f"【问题】: {question}\n"
  6. f"【输入】:{format_context}\n"
  7. "【输出】:只输出 JSON,不要多余文本,格式如下:\n"
  8. "{\n"
  9. ' "id": "id",\n'
  10. ' "claims": [\n'
  11. ' {"point": "事实要点1(尽量原文转述/精准改写)"},\n'
  12. ' {"point": "事实要点2"}\n'
  13. " ],\n"
  14. ' "conflicts_or_limits": ["该片段的限制/含糊点(如时间、定义口径、版本号等)"]\n'
  15. "}"
  16. )
  17. return prompt
  18. def reduce_prompt(question: str, mapped_results_json_list: Optional[str]) -> str:
  19. prompt = (
  20. "【任务】:合并多份 Map 结果,完成以下三点:\n"
  21. "1) 去重并合并同义要点;\n"
  22. "2) 标注并归纳冲突点;\n"
  23. "3) 输出最终回答(含引用)。\n"
  24. f"【问题】:{question}\n"
  25. f"【Map 结果】:{mapped_results_json_list or '[]'}\n"
  26. "【输出(Markdown)】:\n"
  27. "- 简要结论(2-4句)\n"
  28. "- 关键要点(每点附主要引用,如 [Chunk-12] [Chunk-4],用 map 结果中的 id 来表示)\n"
  29. "- 证据信息不一致(如有):列出冲突内容、涉及的 doc_id、可能原因\n"
  30. "- 信息缺口(如有)\n"
  31. )
  32. return prompt
  33. def verify_prompt(question: str, draft: str, formatted_contexts: str) -> str:
  34. prompt = (
  35. "【任务】: 对“初稿答案”的关键断言逐条核验,严格限定仅使用上下文;"
  36. "请标注每条断言是否被证据‘支持/相矛盾/证据不足’,必要时修正结论。\n"
  37. f"【问题】\n{question}\n"
  38. f"【初稿答案】\n{draft}\n"
  39. f"【上下文】\n{formatted_contexts}\n"
  40. "【输出(只输出 JSON)】\n"
  41. "{\n"
  42. ' "verdicts": [\n'
  43. ' {"claim": "断言内容", "status": "supported|contradicted|insufficient", "citations": ["[Chunk-{id}]"]}\n'
  44. " ],\n"
  45. ' "final_answer": "(如需修正,请给出修正后的简明答案,并附引用)"\n'
  46. "}"
  47. )
  48. return prompt
  49. def merge_prompt(question: str, rag_answer: str, formatted_contexts: str) -> str:
  50. """
  51. 合并策略:
  52. 1) 先判定 RAG 初稿是否“足够/部分/不足”;
  53. 2) 若不足或部分,用通用大模型(允许常识/公开资料)补充‘外部要点’,并标注为 [EXT];
  54. 3) 产出融合后的最终答案,优先采用带 [C{id}] 的已证据支撑结论;
  55. 4) 输出审计区:区分 RAG 引用与外部补充,列出信息缺口。
  56. """
  57. prompt = (
  58. "【任务】:先判断 RAG 初稿是否足以回答问题;若不足或仅部分覆盖,则在“允许使用常识/公开资料”的前提下补充,并与上下文证据进行“无缝融合”,产出一份单一成稿(禁止把上下文与外部补充分栏或分章节呈现)。\n"
  59. "【必须遵守】\n"
  60. "- 严禁以“基于上下文/外部补充/对标/工具列表”等标题分隔来源内容;最终仅保留一份融合后的成稿。\n"
  61. "- 句内引用:凡是有依据的关键信息,紧随其后附上来源标记;上下文证据用 [Chunk-{id}],外部补充用 [EXT],可选在括号中加来源类型(如[EXT-百科]、[EXT-官网])。\n"
  62. "- 证据优先级:优先采用【上下文】已被证实的结论;若与外部资料冲突,优先保留【上下文】并在句末以 [EXT-Conflict] 标注冲突存在。\n"
  63. "- 去重与融合:对上下文与外部的相似要点进行合并表述,避免重复罗列;统一术语与口径。\n"
  64. "- 若信息仍不足,明确列出“信息缺口”。\n"
  65. "- 最终答案必须是可直接给用户的完整回应(可含小标题/列表),但不得按照来源划分结构。\n\n"
  66. f"【问题】\n{question}\n\n"
  67. f"【RAG 初稿】\n{rag_answer}\n\n"
  68. f"【上下文】\n{formatted_contexts}\n\n"
  69. # "【写作要求】\n"
  70. # "- 开头 1–2 句给出结论或核心抓手;随后组织为自然段或要点清单(3–8 条)。\n"
  71. # "- 每条关键结论或数据点后紧跟其来源标记;能不标就不写空标注。\n"
  72. # "- 语气务实、可执行,避免流水账与堆砌名词。\n"
  73. # "- 禁止出现“基于上下文…/外部补充…”等来源分栏表述。\n\n"
  74. "【输出(只输出 JSON)】\n"
  75. "{\n"
  76. ' "status": "判断最终结果是否能回答用户问题,若能回答输出 1,若不能则输出 0",\n'
  77. ' "reason": "判断的理由(例如:核心问题是否被覆盖、关键数据/定义是否缺失)",\n'
  78. ' "final_answer_markdown": "融合后的最终答案,不要输出信息缺口(每一条信息需标注 [Chunk-{id}] 与 [EXT])",\n'
  79. ' "audit": {\n'
  80. ' "rag_citations": ["[Chunk-12]", "[Chunk-4]"],\n'
  81. ' "external_claims": [\n'
  82. ' {"claim":"外部补充要点1","note":"来源类型/可信度(如:官网/百科/标准) [EXT]"},\n'
  83. ' {"claim":"外部补充要点2","note":"… [EXT]"}\n'
  84. " ],\n"
  85. ' "gaps": ["仍然缺失或无法确认的信息"]\n'
  86. " }\n"
  87. "}\n"
  88. "【合规自检(执行后自检,不输出给用户)】:若发现答案结构仍以来源分栏或出现“外部补充/基于上下文”等分栏标题,则立即重写为单一成稿。"
  89. )
  90. return prompt
  91. def build_rag_prompt(
  92. question: str,
  93. contexts: List[Dict],
  94. mode: str = "single", # single | map | reduce | rerank | verify | merge
  95. max_chars_per_chunk: int = 800,
  96. draft_answer: Optional[str] = None, # verify/merge 用:RAG 初稿或草稿
  97. mapped_results: Optional[str] = None, # reduce 用
  98. ) -> Dict[str, Any]:
  99. """
  100. 生成 RAG 聚合阶段所需的提示词(Prompt)。
  101. 返回值依据 mode 不同而不同:
  102. - single: {"system": str, "user": str}
  103. - map: {"system": str, "user_list": List[str]} # 每个片段一条 Map 提示
  104. - reduce: {"system": str, "user": str}
  105. - rerank: {"system": str, "user": str}
  106. - verify: {"system": str, "user": str}
  107. - merge: {"system": str, "user": str}
  108. """
  109. # —— 辅助函数 ——
  110. def _trim(text: str, limit: int) -> str:
  111. text = (text or "").strip().replace("\n", " ")
  112. return text if len(text) <= limit else text[: max(0, limit - 1)] + "…"
  113. def _format_each_chunk(chunk: Dict) -> str:
  114. # 兼容不同返回字段:content / text
  115. content = chunk.get("content") or chunk.get("text") or ""
  116. bits = [f"Chunk id={chunk.get('id', '')}"]
  117. if "score" in chunk and chunk["score"] is not None:
  118. try:
  119. bits.append(f"score={round(float(chunk['score']), 4)}")
  120. except Exception:
  121. pass
  122. if chunk.get("url"):
  123. bits.append(f"url={chunk['url']}")
  124. prefix = "[" + " ".join(bits) + "]"
  125. snippet = _trim(content, max_chars_per_chunk)
  126. return f"{prefix}\n{snippet}\n"
  127. def _format_contexts(chunks: List[Dict]) -> str:
  128. return "\n".join(_format_each_chunk(c) for c in chunks).strip()
  129. # —— 统一 System 约束(全中文) ——
  130. system_text = (
  131. "你是一位“基于证据的助手”。你必须只使用我提供的【上下文】来回答:\n"
  132. "- 不得使用外部常识或臆测;\n"
  133. "- 若上下文不足,请明确输出“信息不足”,并指出缺失的信息类型;\n"
  134. "- 对关键结论附 [Chunk-{id}] 形式的出处;\n"
  135. "- 如存在冲突证据,请列出冲突并给出谨慎结论与采信依据;\n"
  136. "- 用中文回答,保持简洁、结构化。"
  137. )
  138. formatted_contexts = _format_contexts(contexts)
  139. # —— 各模式分发 ——
  140. if mode == "single":
  141. user_text = (
  142. f"【问题】\n{question}\n\n"
  143. f"【上下文(已按相关性排序)】\n{formatted_contexts}\n\n"
  144. "【请按以下结构作答】\n"
  145. "1) 简要结论(2-4句)\n"
  146. "2) 关键要点(每点附1-2个引用,如 [C3])\n"
  147. "3) 证据信息不一致(如有)\n"
  148. "4) 信息缺口(如有)"
  149. )
  150. return {"system": system_text, "user": user_text}
  151. if mode == "map":
  152. map_user_list = []
  153. for ctx in contexts:
  154. format_context = _format_each_chunk(ctx)
  155. map_user_list.append(map_prompt(question, format_context))
  156. return {"system": system_text, "user_list": map_user_list}
  157. if mode == "reduce":
  158. return {"system": system_text, "user": reduce_prompt(question, mapped_results)}
  159. if mode == "rerank":
  160. rerank_system = "你是一位严谨的重排器。请只输出 JSON。"
  161. rerank_user = (
  162. f"请比较下列候选段与问题“{question}”的相关性,仅打分并排序(不做总结)。\n"
  163. "评分标准(由高到低):直接回答性 > 主题一致性 > 细节重合度 > 时间匹配。\n\n"
  164. f"【候选段】\n{formatted_contexts}\n\n"
  165. "【只输出 JSON,格式如下(按 score 从高到低)】\n"
  166. '[{"id":"DOC_ID","score":X.X}]'
  167. )
  168. return {"system": rerank_system, "user": rerank_user}
  169. if mode == "verify":
  170. draft = draft_answer or "(此处为初稿答案)"
  171. return {
  172. "system": system_text,
  173. "user": verify_prompt(question, draft, formatted_contexts),
  174. }
  175. if mode == "merge":
  176. if not draft_answer:
  177. raise ValueError("merge 模式需要提供 draft_answer(即 RAG 初稿答案)。")
  178. return {
  179. "system": system_text,
  180. "user": merge_prompt(question, draft_answer, formatted_contexts),
  181. }
  182. raise ValueError(f"不支持的模式:{mode}")
  183. __all__ = ["build_rag_prompt"]