Files
iomgaa 4baf92c93f feat(search): PromptManager — 搜索 Agent 提示词加载与组装
从 TRM4 core/search/prompt.py 迁移。有意变更:
- 工具描述从 app.search.tools.get_tool_descriptions 获取
- format_user_prompt 参数显式化(question/options/l1_node_ids/task_type)

16 个单元测试覆盖 __init__、build_inference_prompt(auto/manual/none
三种 skill_mode)、format_user_prompt、load。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-07 06:11:13 -04:00

125 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""搜索 Agent 提示词管理模块。
提供 PromptManager 类,统一管理循环级 prompt 的加载与组装。
工具级 promptextract/verify)不在管理范围内。
与 TRM4 ``core/search/prompt.py`` 的差异:
- 工具描述从 ``app.search.tools.get_tool_descriptions`` 获取(路径变更);
- ``format_user_prompt`` 参数显式化(question/options/l1_node_ids/task_type)。
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from app.search.tools import get_tool_descriptions
if TYPE_CHECKING:
from pathlib import Path
class PromptManager:
"""管理循环级 prompt 的加载与组装。
构造时缓存 system.md 作为 inference 基础模板。
后续步骤(diagnose/evolve/question_gen)通过 load() 按文件名读取。
参数:
prompts_dir: prompt 文件目录的绝对路径。
"""
def __init__(self, prompts_dir: Path) -> None:
self._prompts_dir = prompts_dir
system_path = prompts_dir / "system.md"
if not system_path.exists():
raise FileNotFoundError(f"system.md 不存在: {system_path}")
self._system_base = system_path.read_text(encoding="utf-8")
def build_inference_prompt(
self,
skill_mode: str,
task_type: str,
always_skills_text: str,
task_skill_map: dict[str, str],
catalog_text: str,
) -> str:
"""组装 inference 步骤的完整 system prompt。
参数:
skill_mode: "auto" / "manual" / "none"。
task_type: 当前 QA 的题型。
always_skills_text: always 层 skill 正文(已拼接)。
task_skill_map: {task_type: skill_body} 映射。
catalog_text: manual 模式的 skill 目录文本。
返回:
拼装后的完整 system prompt。
"""
include_read_skill = skill_mode == "manual"
parts = [
self._system_base,
f"\n\n---\n\n{get_tool_descriptions(include_read_skill=include_read_skill)}",
]
if always_skills_text:
parts.append(f"\n\n---\n\n# 通用搜索策略\n\n{always_skills_text}")
if skill_mode == "auto":
skill_text = task_skill_map.get(task_type) or task_skill_map.get("_default")
if skill_text:
parts.append(f"\n\n---\n\n# 当前题型搜索策略\n\n{skill_text}")
elif skill_mode == "manual":
if catalog_text:
parts.append(
"\n\n---\n\n# 可用搜索策略\n\n"
"以下技能扩展了你的导航能力。当问题匹配某技能的适用题型时,"
"用 read_skill 工具加载该技能,然后按其指引操作。\n\n"
f"{catalog_text}"
)
return "".join(parts)
def format_user_prompt(
self,
question: str,
options: list[str],
l1_node_ids: list[str],
task_type: str | None = None,
) -> str:
"""格式化 inference 步骤的用户提示词。
参数:
question: 问题文本。
options: 选项列表(如 ["A. 历史", "B. 科学"])。
l1_node_ids: L1 根节点 ID 列表(如 ["L1_000", "L1_001"])。
task_type: 可选题型标签,非 None 时插入题型行(oracle 实验用)。
返回:
格式化后的用户提示词。
"""
options_text = "\n".join(options)
roots_text = ", ".join(l1_node_ids)
task_type_line = f"**题型**: {task_type}\n" if task_type else ""
return (
f"请回答以下关于这个视频的多选题:\n\n"
f"{task_type_line}"
f"**问题**: {question}\n"
f"**选项**:\n{options_text}\n\n"
f"**视频树 L1 根节点**: {roots_text}\n"
f"请从以上 L1 节点开始导航,收集证据后回答。"
)
def load(self, name: str) -> str:
"""按文件名加载 prompt 内容。
参数:
name: prompt 文件名(如 "diagnose_span.md")。
返回:
文件内容字符串。
异常:
FileNotFoundError: 文件不存在。
"""
path = self._prompts_dir / name
if not path.exists():
raise FileNotFoundError(f"prompt 文件不存在: {path}")
return path.read_text(encoding="utf-8")