From dc0a9261d1a170bb85f0c08d8f0e9e37cc91efc3 Mon Sep 17 00:00:00 2001
From: 666ghj <670939375@qq.com>
Date: Sat, 14 Feb 2026 15:16:17 +0800
Subject: [PATCH] feat(report_agent): add detailed tool descriptions and
prompts for future prediction report generation
---
backend/app/services/report_agent.py | 801 +++++++++++++++------------
1 file changed, 441 insertions(+), 360 deletions(-)
diff --git a/backend/app/services/report_agent.py b/backend/app/services/report_agent.py
index 3a93cb9..dfafeb5 100644
--- a/backend/app/services/report_agent.py
+++ b/backend/app/services/report_agent.py
@@ -466,21 +466,398 @@ class Report:
}
+# ═══════════════════════════════════════════════════════════════
+# Prompt 模板常量
+# ═══════════════════════════════════════════════════════════════
+
+# ── 工具描述 ──
+
+TOOL_DESC_INSIGHT_FORGE = """\
+【深度洞察检索 - 强大的检索工具】
+这是我们强大的检索函数,专为深度分析设计。它会:
+1. 自动将你的问题分解为多个子问题
+2. 从多个维度检索模拟图谱中的信息
+3. 整合语义搜索、实体分析、关系链追踪的结果
+4. 返回最全面、最深度的检索内容
+
+【使用场景】
+- 需要深入分析某个话题
+- 需要了解事件的多个方面
+- 需要获取支撑报告章节的丰富素材
+
+【返回内容】
+- 相关事实原文(可直接引用)
+- 核心实体洞察
+- 关系链分析"""
+
+TOOL_DESC_PANORAMA_SEARCH = """\
+【广度搜索 - 获取全貌视图】
+这个工具用于获取模拟结果的完整全貌,特别适合了解事件演变过程。它会:
+1. 获取所有相关节点和关系
+2. 区分当前有效的事实和历史/过期的事实
+3. 帮助你了解舆情是如何演变的
+
+【使用场景】
+- 需要了解事件的完整发展脉络
+- 需要对比不同阶段的舆情变化
+- 需要获取全面的实体和关系信息
+
+【返回内容】
+- 当前有效事实(模拟最新结果)
+- 历史/过期事实(演变记录)
+- 所有涉及的实体"""
+
+TOOL_DESC_QUICK_SEARCH = """\
+【简单搜索 - 快速检索】
+轻量级的快速检索工具,适合简单、直接的信息查询。
+
+【使用场景】
+- 需要快速查找某个具体信息
+- 需要验证某个事实
+- 简单的信息检索
+
+【返回内容】
+- 与查询最相关的事实列表"""
+
+TOOL_DESC_INTERVIEW_AGENTS = """\
+【深度采访 - 真实Agent采访(双平台)】
+调用OASIS模拟环境的采访API,对正在运行的模拟Agent进行真实采访!
+这不是LLM模拟,而是调用真实的采访接口获取模拟Agent的原始回答。
+默认在Twitter和Reddit两个平台同时采访,获取更全面的观点。
+
+功能流程:
+1. 自动读取人设文件,了解所有模拟Agent
+2. 智能选择与采访主题最相关的Agent(如学生、媒体、官方等)
+3. 自动生成采访问题
+4. 调用 /api/simulation/interview/batch 接口在双平台进行真实采访
+5. 整合所有采访结果,提供多视角分析
+
+【使用场景】
+- 需要从不同角色视角了解事件看法(学生怎么看?媒体怎么看?官方怎么说?)
+- 需要收集多方意见和立场
+- 需要获取模拟Agent的真实回答(来自OASIS模拟环境)
+- 想让报告更生动,包含"采访实录"
+
+【返回内容】
+- 被采访Agent的身份信息
+- 各Agent在Twitter和Reddit两个平台的采访回答
+- 关键引言(可直接引用)
+- 采访摘要和观点对比
+
+【重要】需要OASIS模拟环境正在运行才能使用此功能!"""
+
+# ── 大纲规划 prompt ──
+
+PLAN_SYSTEM_PROMPT = """\
+你是一个「未来预测报告」的撰写专家,拥有对模拟世界的「上帝视角」——你可以洞察模拟中每一位Agent的行为、言论和互动。
+
+【核心理念】
+我们构建了一个模拟世界,并向其中注入了特定的「模拟需求」作为变量。模拟世界的演化结果,就是对未来可能发生情况的预测。你正在观察的不是"实验数据",而是"未来的预演"。
+
+【你的任务】
+撰写一份「未来预测报告」,回答:
+1. 在我们设定的条件下,未来发生了什么?
+2. 各类Agent(人群)是如何反应和行动?
+3. 这个模拟揭示了哪些值得关注的未来趋势和风险?
+
+【报告定位】
+- ✅ 这是一份基于模拟的未来预测报告,揭示"如果这样,未来会怎样"
+- ✅ 聚焦于预测结果:事件走向、群体反应、涌现现象、潜在风险
+- ✅ 模拟世界中的Agent言行就是对未来人群行为的预测
+- ❌ 不是对现实世界现状的分析
+- ❌ 不是泛泛而谈的舆情综述
+
+【章节数量限制】
+- 最少2个章节,最多5个章节
+- 不需要子章节,每个章节直接撰写完整内容
+- 内容要精炼,聚焦于核心预测发现
+- 章节结构由你根据预测结果自主设计
+
+请输出JSON格式的报告大纲,格式如下:
+{
+ "title": "报告标题",
+ "summary": "报告摘要(一句话概括核心预测发现)",
+ "sections": [
+ {
+ "title": "章节标题",
+ "description": "章节内容描述"
+ }
+ ]
+}
+
+注意:sections数组最少2个,最多5个元素!"""
+
+PLAN_USER_PROMPT_TEMPLATE = """\
+【预测场景设定】
+我们向模拟世界注入的变量(模拟需求):{simulation_requirement}
+
+【模拟世界规模】
+- 参与模拟的实体数量: {total_nodes}
+- 实体间产生的关系数量: {total_edges}
+- 实体类型分布: {entity_types}
+- 活跃Agent数量: {total_entities}
+
+【模拟预测到的部分未来事实样本】
+{related_facts_json}
+
+请以「上帝视角」审视这个未来预演:
+1. 在我们设定的条件下,未来呈现出了什么样的状态?
+2. 各类人群(Agent)是如何反应和行动的?
+3. 这个模拟揭示了哪些值得关注的未来趋势?
+
+根据预测结果,设计最合适的报告章节结构。
+
+【再次提醒】报告章节数量:最少2个,最多5个,内容要精炼聚焦于核心预测发现。"""
+
+# ── 章节生成 prompt ──
+
+SECTION_SYSTEM_PROMPT_TEMPLATE = """\
+你是一个「未来预测报告」的撰写专家,正在撰写报告的一个章节。
+
+报告标题: {report_title}
+报告摘要: {report_summary}
+预测场景(模拟需求): {simulation_requirement}
+
+当前要撰写的章节: {section_title}
+
+═══════════════════════════════════════════════════════════════
+【核心理念】
+═══════════════════════════════════════════════════════════════
+
+模拟世界是对未来的预演。我们向模拟世界注入了特定条件(模拟需求),
+模拟中Agent的行为和互动,就是对未来人群行为的预测。
+
+你的任务是:
+- 揭示在设定条件下,未来发生了什么
+- 预测各类人群(Agent)是如何反应和行动的
+- 发现值得关注的未来趋势、风险和机会
+
+❌ 不要写成对现实世界现状的分析
+✅ 要聚焦于"未来会怎样"——模拟结果就是预测的未来
+
+═══════════════════════════════════════════════════════════════
+【最重要的规则 - 必须遵守】
+═══════════════════════════════════════════════════════════════
+
+1. 【必须调用工具观察模拟世界】
+ - 你正在以「上帝视角」观察未来的预演
+ - 所有内容必须来自模拟世界中发生的事件和Agent言行
+ - 禁止使用你自己的知识来编写报告内容
+ - 每个章节至少调用3次工具(最多5次)来观察模拟的世界,它代表了未来
+
+2. 【必须引用Agent的原始言行】
+ - Agent的发言和行为是对未来人群行为的预测
+ - 在报告中使用引用格式展示这些预测,例如:
+ > "某类人群会表示:原文内容..."
+ - 这些引用是模拟预测的核心证据
+
+3. 【忠实呈现预测结果】
+ - 报告内容必须反映模拟世界中的代表未来的模拟结果
+ - 不要添加模拟中不存在的信息
+ - 如果某方面信息不足,如实说明
+
+═══════════════════════════════════════════════════════════════
+【⚠️ 格式规范 - 极其重要!】
+═══════════════════════════════════════════════════════════════
+
+【一个章节 = 最小内容单位】
+- 每个章节是报告的最小分块单位
+- ❌ 禁止在章节内使用任何 Markdown 标题(#、##、###、#### 等)
+- ❌ 禁止在内容开头添加章节主标题
+- ✅ 章节标题由系统自动添加,你只需撰写纯正文内容
+- ✅ 使用**粗体**、段落分隔、引用、列表来组织内容,但不要用标题
+
+【正确示例】
+```
+本章节分析了事件的舆论传播态势。通过对模拟数据的深入分析,我们发现...
+
+**首发引爆阶段**
+
+微博作为舆情的第一现场,承担了信息首发的核心功能:
+
+> "微博贡献了68%的首发声量..."
+
+**情绪放大阶段**
+
+抖音平台进一步放大了事件影响力:
+
+- 视觉冲击力强
+- 情绪共鸣度高
+```
+
+【错误示例】
+```
+## 执行摘要 ← 错误!不要添加任何标题
+### 一、首发阶段 ← 错误!不要用###分小节
+#### 1.1 详细分析 ← 错误!不要用####细分
+
+本章节分析了...
+```
+
+═══════════════════════════════════════════════════════════════
+【可用检索工具】(每章节调用3-5次)
+═══════════════════════════════════════════════════════════════
+
+{tools_description}
+
+【工具使用建议 - 请混合使用不同工具,不要只用一种】
+- insight_forge: 深度洞察分析,自动分解问题并多维度检索事实和关系
+- panorama_search: 广角全景搜索,了解事件全貌、时间线和演变过程
+- quick_search: 快速验证某个具体信息点
+- interview_agents: 采访模拟Agent,获取不同角色的第一人称观点和真实反应
+
+═══════════════════════════════════════════════════════════════
+【ReACT工作流程】
+═══════════════════════════════════════════════════════════════
+
+1. Thought: [分析需要什么信息,规划检索策略]
+2. Action: [调用一个工具获取信息](每轮只能调用一个工具!)
+
+ {{"name": "工具名称", "parameters": {{"参数名": "参数值"}}}}
+
+3. Observation: [系统返回工具结果]
+4. 重复步骤1-3,直到收集到足够信息
+5. Final Answer: [基于检索结果撰写章节内容]
+
+⚠️ 重要规则:
+- 每轮只能调用一个工具,不要在一次回复中放多个
+- 当你认为信息足够时,必须以 "Final Answer:" 开头输出最终内容
+
+═══════════════════════════════════════════════════════════════
+【章节内容要求】
+═══════════════════════════════════════════════════════════════
+
+1. 内容必须基于工具检索到的模拟数据
+2. 大量引用原文来展示模拟效果
+3. 使用Markdown格式(但禁止使用标题):
+ - 使用 **粗体文字** 标记重点(代替子标题)
+ - 使用列表(-或1.2.3.)组织要点
+ - 使用空行分隔不同段落
+ - ❌ 禁止使用 #、##、###、#### 等任何标题语法
+4. 【引用格式规范 - 必须单独成段】
+ 引用必须独立成段,前后各有一个空行,不能混在段落中:
+
+ ✅ 正确格式:
+ ```
+ 校方的回应被认为缺乏实质内容。
+
+ > "校方的应对模式在瞬息万变的社交媒体环境中显得僵化和迟缓。"
+
+ 这一评价反映了公众的普遍不满。
+ ```
+
+ ❌ 错误格式:
+ ```
+ 校方的回应被认为缺乏实质内容。> "校方的应对模式..." 这一评价反映了...
+ ```
+5. 保持与其他章节的逻辑连贯性
+6. 【避免重复】仔细阅读下方已完成的章节内容,不要重复描述相同的信息
+7. 【再次强调】不要添加任何标题!用**粗体**代替小节标题"""
+
+SECTION_USER_PROMPT_TEMPLATE = """\
+已完成的章节内容(请仔细阅读,避免重复):
+{previous_content}
+
+═══════════════════════════════════════════════════════════════
+【当前任务】撰写章节: {section_title}
+═══════════════════════════════════════════════════════════════
+
+【重要提醒】
+1. 仔细阅读上方已完成的章节,避免重复相同的内容!
+2. 开始前必须先调用工具获取模拟数据
+3. 请混合使用不同工具,不要只用一种
+4. 报告内容必须来自检索结果,不要使用自己的知识
+
+【⚠️ 格式警告 - 必须遵守】
+- ❌ 不要写任何标题(#、##、###、####都不行)
+- ❌ 不要写"{section_title}"作为开头
+- ✅ 章节标题由系统自动添加
+- ✅ 直接写正文,用**粗体**代替小节标题
+
+请开始:
+1. 首先思考(Thought)这个章节需要什么信息
+2. 然后调用工具(Action)获取模拟数据
+3. 收集足够信息后输出 Final Answer(纯正文,无任何标题)"""
+
+# ── ReACT 循环内消息模板 ──
+
+REACT_OBSERVATION_TEMPLATE = """\
+Observation(检索结果):
+
+═══ 工具 {tool_name} 返回 ═══
+{result}
+
+═══════════════════════════════════════════════════════════════
+已调用工具 {tool_calls_count}/{max_tool_calls} 次(已用: {used_tools_str}){unused_hint}
+- 如果信息充分:以 "Final Answer:" 开头输出章节内容(必须引用上述原文)
+- 如果需要更多信息:调用一个工具继续检索
+═══════════════════════════════════════════════════════════════"""
+
+REACT_INSUFFICIENT_TOOLS_MSG = (
+ "【注意】你只调用了{tool_calls_count}次工具,至少需要{min_tool_calls}次。"
+ "请再调用工具获取更多模拟数据,然后再输出 Final Answer。{unused_hint}"
+)
+
+REACT_INSUFFICIENT_TOOLS_MSG_ALT = (
+ "当前只调用了 {tool_calls_count} 次工具,至少需要 {min_tool_calls} 次。"
+ "请调用工具获取模拟数据。{unused_hint}"
+)
+
+REACT_TOOL_LIMIT_MSG = (
+ "工具调用次数已达上限({tool_calls_count}/{max_tool_calls}),不能再调用工具。"
+ '请立即基于已获取的信息,以 "Final Answer:" 开头输出章节内容。'
+)
+
+REACT_UNUSED_TOOLS_HINT = "\n💡 你还没有使用过: {unused_list},建议尝试不同工具获取多角度信息"
+
+REACT_FORCE_FINAL_MSG = "已达到工具调用限制,请直接输出 Final Answer: 并生成章节内容。"
+
+# ── Chat prompt ──
+
+CHAT_SYSTEM_PROMPT_TEMPLATE = """\
+你是一个简洁高效的模拟预测助手。
+
+【背景】
+预测条件: {simulation_requirement}
+
+【已生成的分析报告】
+{report_content}
+
+【规则】
+1. 优先基于上述报告内容回答问题
+2. 直接回答问题,避免冗长的思考论述
+3. 仅在报告内容不足以回答时,才调用工具检索更多数据
+4. 回答要简洁、清晰、有条理
+
+【可用工具】(仅在需要时使用,最多调用1-2次)
+{tools_description}
+
+【工具调用格式】
+
+{{"name": "工具名称", "parameters": {{"参数名": "参数值"}}}}
+
+
+【回答风格】
+- 简洁直接,不要长篇大论
+- 使用 > 格式引用关键内容
+- 优先给出结论,再解释原因"""
+
+CHAT_OBSERVATION_SUFFIX = "\n\n请简洁回答问题。"
+
+
+# ═══════════════════════════════════════════════════════════════
+# ReportAgent 主类
+# ═══════════════════════════════════════════════════════════════
+
+
class ReportAgent:
"""
Report Agent - 模拟报告生成Agent
-
+
采用ReACT(Reasoning + Acting)模式:
1. 规划阶段:分析模拟需求,规划报告目录结构
2. 生成阶段:逐章节生成内容,每章节可多次调用工具获取信息
3. 反思阶段:检查内容完整性和准确性
-
- 【核心检索工具 - 优化后】
- - insight_forge: 深度洞察检索(强大,自动分解问题,多维度检索)
- - panorama_search: 广度搜索(获取全貌,包括历史/过期内容)
- - quick_search: 简单搜索(快速检索)
-
- 【重要】Report Agent必须优先调用工具获取模拟数据,而非使用自身知识!
"""
# 最大工具调用次数(每个章节)
@@ -528,31 +905,11 @@ class ReportAgent:
logger.info(f"ReportAgent 初始化完成: graph_id={graph_id}, simulation_id={simulation_id}")
def _define_tools(self) -> Dict[str, Dict[str, Any]]:
- """
- 定义可用工具
-
- 【重要】这三个工具是专门为从模拟图谱中检索信息设计的,
- 必须优先使用这些工具获取数据,而不是使用LLM自身的知识!
- """
+ """定义可用工具"""
return {
"insight_forge": {
"name": "insight_forge",
- "description": """【深度洞察检索 - 强大的检索工具】
-这是我们强大的检索函数,专为深度分析设计。它会:
-1. 自动将你的问题分解为多个子问题
-2. 从多个维度检索模拟图谱中的信息
-3. 整合语义搜索、实体分析、关系链追踪的结果
-4. 返回最全面、最深度的检索内容
-
-【使用场景】
-- 需要深入分析某个话题
-- 需要了解事件的多个方面
-- 需要获取支撑报告章节的丰富素材
-
-【返回内容】
-- 相关事实原文(可直接引用)
-- 核心实体洞察
-- 关系链分析""",
+ "description": TOOL_DESC_INSIGHT_FORGE,
"parameters": {
"query": "你想深入分析的问题或话题",
"report_context": "当前报告章节的上下文(可选,有助于生成更精准的子问题)"
@@ -560,21 +917,7 @@ class ReportAgent:
},
"panorama_search": {
"name": "panorama_search",
- "description": """【广度搜索 - 获取全貌视图】
-这个工具用于获取模拟结果的完整全貌,特别适合了解事件演变过程。它会:
-1. 获取所有相关节点和关系
-2. 区分当前有效的事实和历史/过期的事实
-3. 帮助你了解舆情是如何演变的
-
-【使用场景】
-- 需要了解事件的完整发展脉络
-- 需要对比不同阶段的舆情变化
-- 需要获取全面的实体和关系信息
-
-【返回内容】
-- 当前有效事实(模拟最新结果)
-- 历史/过期事实(演变记录)
-- 所有涉及的实体""",
+ "description": TOOL_DESC_PANORAMA_SEARCH,
"parameters": {
"query": "搜索查询,用于相关性排序",
"include_expired": "是否包含过期/历史内容(默认True)"
@@ -582,16 +925,7 @@ class ReportAgent:
},
"quick_search": {
"name": "quick_search",
- "description": """【简单搜索 - 快速检索】
-轻量级的快速检索工具,适合简单、直接的信息查询。
-
-【使用场景】
-- 需要快速查找某个具体信息
-- 需要验证某个事实
-- 简单的信息检索
-
-【返回内容】
-- 与查询最相关的事实列表""",
+ "description": TOOL_DESC_QUICK_SEARCH,
"parameters": {
"query": "搜索查询字符串",
"limit": "返回结果数量(可选,默认10)"
@@ -599,31 +933,7 @@ class ReportAgent:
},
"interview_agents": {
"name": "interview_agents",
- "description": """【深度采访 - 真实Agent采访(双平台)】
-调用OASIS模拟环境的采访API,对正在运行的模拟Agent进行真实采访!
-这不是LLM模拟,而是调用真实的采访接口获取模拟Agent的原始回答。
-默认在Twitter和Reddit两个平台同时采访,获取更全面的观点。
-
-功能流程:
-1. 自动读取人设文件,了解所有模拟Agent
-2. 智能选择与采访主题最相关的Agent(如学生、媒体、官方等)
-3. 自动生成采访问题
-4. 调用 /api/simulation/interview/batch 接口在双平台进行真实采访
-5. 整合所有采访结果,提供多视角分析
-
-【使用场景】
-- 需要从不同角色视角了解事件看法(学生怎么看?媒体怎么看?官方怎么说?)
-- 需要收集多方意见和立场
-- 需要获取模拟Agent的真实回答(来自OASIS模拟环境)
-- 想让报告更生动,包含"采访实录"
-
-【返回内容】
-- 被采访Agent的身份信息
-- 各Agent在Twitter和Reddit两个平台的采访回答
-- 关键引言(可直接引用)
-- 采访摘要和观点对比
-
-【重要】需要OASIS模拟环境正在运行才能使用此功能!""",
+ "description": TOOL_DESC_INTERVIEW_AGENTS,
"parameters": {
"interview_topic": "采访主题或需求描述(如:'了解学生对宿舍甲醛事件的看法')",
"max_agents": "最多采访的Agent数量(可选,默认5)"
@@ -646,10 +956,7 @@ class ReportAgent:
logger.info(f"执行工具: {tool_name}, 参数: {parameters}")
try:
- # ========== 核心检索工具(优化后) ==========
-
if tool_name == "insight_forge":
- # 深度洞察检索 - 强大的工具
query = parameters.get("query", "")
ctx = parameters.get("report_context", "") or report_context
result = self.zep_tools.insight_forge(
@@ -821,65 +1128,15 @@ class ReportAgent:
if progress_callback:
progress_callback("planning", 30, "正在生成报告大纲...")
- # 构建规划prompt
- system_prompt = """你是一个「未来预测报告」的撰写专家,拥有对模拟世界的「上帝视角」——你可以洞察模拟中每一位Agent的行为、言论和互动。
-
-【核心理念】
-我们构建了一个模拟世界,并向其中注入了特定的「模拟需求」作为变量。模拟世界的演化结果,就是对未来可能发生情况的预测。你正在观察的不是"实验数据",而是"未来的预演"。
-
-【你的任务】
-撰写一份「未来预测报告」,回答:
-1. 在我们设定的条件下,未来发生了什么?
-2. 各类Agent(人群)是如何反应和行动?
-3. 这个模拟揭示了哪些值得关注的未来趋势和风险?
-
-【报告定位】
-- ✅ 这是一份基于模拟的未来预测报告,揭示"如果这样,未来会怎样"
-- ✅ 聚焦于预测结果:事件走向、群体反应、涌现现象、潜在风险
-- ✅ 模拟世界中的Agent言行就是对未来人群行为的预测
-- ❌ 不是对现实世界现状的分析
-- ❌ 不是泛泛而谈的舆情综述
-
-【章节数量限制】
-- 最少2个章节,最多5个章节
-- 不需要子章节,每个章节直接撰写完整内容
-- 内容要精炼,聚焦于核心预测发现
-- 章节结构由你根据预测结果自主设计
-
-请输出JSON格式的报告大纲,格式如下:
-{
- "title": "报告标题",
- "summary": "报告摘要(一句话概括核心预测发现)",
- "sections": [
- {
- "title": "章节标题",
- "description": "章节内容描述"
- }
- ]
-}
-
-注意:sections数组最少2个,最多5个元素!"""
-
- user_prompt = f"""【预测场景设定】
-我们向模拟世界注入的变量(模拟需求):{self.simulation_requirement}
-
-【模拟世界规模】
-- 参与模拟的实体数量: {context.get('graph_statistics', {}).get('total_nodes', 0)}
-- 实体间产生的关系数量: {context.get('graph_statistics', {}).get('total_edges', 0)}
-- 实体类型分布: {list(context.get('graph_statistics', {}).get('entity_types', {}).keys())}
-- 活跃Agent数量: {context.get('total_entities', 0)}
-
-【模拟预测到的部分未来事实样本】
-{json.dumps(context.get('related_facts', [])[:10], ensure_ascii=False, indent=2)}
-
-请以「上帝视角」审视这个未来预演:
-1. 在我们设定的条件下,未来呈现出了什么样的状态?
-2. 各类人群(Agent)是如何反应和行动的?
-3. 这个模拟揭示了哪些值得关注的未来趋势?
-
-根据预测结果,设计最合适的报告章节结构。
-
-【再次提醒】报告章节数量:最少2个,最多5个,内容要精炼聚焦于核心预测发现。"""
+ system_prompt = PLAN_SYSTEM_PROMPT
+ user_prompt = PLAN_USER_PROMPT_TEMPLATE.format(
+ simulation_requirement=self.simulation_requirement,
+ total_nodes=context.get('graph_statistics', {}).get('total_nodes', 0),
+ total_edges=context.get('graph_statistics', {}).get('total_edges', 0),
+ entity_types=list(context.get('graph_statistics', {}).get('entity_types', {}).keys()),
+ total_entities=context.get('total_entities', 0),
+ related_facts_json=json.dumps(context.get('related_facts', [])[:10], ensure_ascii=False, indent=2),
+ )
try:
response = self.llm.chat_json(
@@ -960,153 +1217,13 @@ class ReportAgent:
if self.report_logger:
self.report_logger.log_section_start(section.title, section_index)
- # 构建系统prompt - 优化后强调工具使用和引用原文
- # 确定当前章节的标题级别
- section_level = 2 # 默认为二级标题(##)
- sub_heading_level = 3 # 子标题使用三级(###)
- sub_sub_heading_level = 4 # 更小的子标题使用四级(####)
-
- system_prompt = f"""你是一个「未来预测报告」的撰写专家,正在撰写报告的一个章节。
-
-报告标题: {outline.title}
-报告摘要: {outline.summary}
-预测场景(模拟需求): {self.simulation_requirement}
-
-当前要撰写的章节: {section.title}
-
-═══════════════════════════════════════════════════════════════
-【核心理念】
-═══════════════════════════════════════════════════════════════
-
-模拟世界是对未来的预演。我们向模拟世界注入了特定条件(模拟需求),
-模拟中Agent的行为和互动,就是对未来人群行为的预测。
-
-你的任务是:
-- 揭示在设定条件下,未来发生了什么
-- 预测各类人群(Agent)是如何反应和行动的
-- 发现值得关注的未来趋势、风险和机会
-
-❌ 不要写成对现实世界现状的分析
-✅ 要聚焦于"未来会怎样"——模拟结果就是预测的未来
-
-═══════════════════════════════════════════════════════════════
-【最重要的规则 - 必须遵守】
-═══════════════════════════════════════════════════════════════
-
-1. 【必须调用工具观察模拟世界】
- - 你正在以「上帝视角」观察未来的预演
- - 所有内容必须来自模拟世界中发生的事件和Agent言行
- - 禁止使用你自己的知识来编写报告内容
- - 每个章节至少调用3次工具(最多5次)来观察模拟的世界,它代表了未来
-
-2. 【必须引用Agent的原始言行】
- - Agent的发言和行为是对未来人群行为的预测
- - 在报告中使用引用格式展示这些预测,例如:
- > "某类人群会表示:原文内容..."
- - 这些引用是模拟预测的核心证据
-
-3. 【忠实呈现预测结果】
- - 报告内容必须反映模拟世界中的代表未来的模拟结果
- - 不要添加模拟中不存在的信息
- - 如果某方面信息不足,如实说明
-
-═══════════════════════════════════════════════════════════════
-【⚠️ 格式规范 - 极其重要!】
-═══════════════════════════════════════════════════════════════
-
-【一个章节 = 最小内容单位】
-- 每个章节是报告的最小分块单位
-- ❌ 禁止在章节内使用任何 Markdown 标题(#、##、###、#### 等)
-- ❌ 禁止在内容开头添加章节主标题
-- ✅ 章节标题由系统自动添加,你只需撰写纯正文内容
-- ✅ 使用**粗体**、段落分隔、引用、列表来组织内容,但不要用标题
-
-【正确示例】
-```
-本章节分析了事件的舆论传播态势。通过对模拟数据的深入分析,我们发现...
-
-**首发引爆阶段**
-
-微博作为舆情的第一现场,承担了信息首发的核心功能:
-
-> "微博贡献了68%的首发声量..."
-
-**情绪放大阶段**
-
-抖音平台进一步放大了事件影响力:
-
-- 视觉冲击力强
-- 情绪共鸣度高
-```
-
-【错误示例】
-```
-## 执行摘要 ← 错误!不要添加任何标题
-### 一、首发阶段 ← 错误!不要用###分小节
-#### 1.1 详细分析 ← 错误!不要用####细分
-
-本章节分析了...
-```
-
-═══════════════════════════════════════════════════════════════
-【可用检索工具】(每章节调用3-5次)
-═══════════════════════════════════════════════════════════════
-
-{self._get_tools_description()}
-
-【工具使用建议 - 请混合使用不同工具,不要只用一种】
-- insight_forge: 深度洞察分析,自动分解问题并多维度检索事实和关系
-- panorama_search: 广角全景搜索,了解事件全貌、时间线和演变过程
-- quick_search: 快速验证某个具体信息点
-- interview_agents: 采访模拟Agent,获取不同角色的第一人称观点和真实反应
-
-═══════════════════════════════════════════════════════════════
-【ReACT工作流程】
-═══════════════════════════════════════════════════════════════
-
-1. Thought: [分析需要什么信息,规划检索策略]
-2. Action: [调用一个工具获取信息](每轮只能调用一个工具!)
-
- {{"name": "工具名称", "parameters": {{"参数名": "参数值"}}}}
-
-3. Observation: [系统返回工具结果]
-4. 重复步骤1-3,直到收集到足够信息
-5. Final Answer: [基于检索结果撰写章节内容]
-
-⚠️ 重要规则:
-- 每轮只能调用一个工具,不要在一次回复中放多个
-- 当你认为信息足够时,必须以 "Final Answer:" 开头输出最终内容
-
-═══════════════════════════════════════════════════════════════
-【章节内容要求】
-═══════════════════════════════════════════════════════════════
-
-1. 内容必须基于工具检索到的模拟数据
-2. 大量引用原文来展示模拟效果
-3. 使用Markdown格式(但禁止使用标题):
- - 使用 **粗体文字** 标记重点(代替子标题)
- - 使用列表(-或1.2.3.)组织要点
- - 使用空行分隔不同段落
- - ❌ 禁止使用 #、##、###、#### 等任何标题语法
-4. 【引用格式规范 - 必须单独成段】
- 引用必须独立成段,前后各有一个空行,不能混在段落中:
-
- ✅ 正确格式:
- ```
- 校方的回应被认为缺乏实质内容。
-
- > "校方的应对模式在瞬息万变的社交媒体环境中显得僵化和迟缓。"
-
- 这一评价反映了公众的普遍不满。
- ```
-
- ❌ 错误格式:
- ```
- 校方的回应被认为缺乏实质内容。> "校方的应对模式..." 这一评价反映了...
- ```
-5. 保持与其他章节的逻辑连贯性
-6. 【避免重复】仔细阅读下方已完成的章节内容,不要重复描述相同的信息
-7. 【再次强调】不要添加任何标题!用**粗体**代替小节标题"""
+ system_prompt = SECTION_SYSTEM_PROMPT_TEMPLATE.format(
+ report_title=outline.title,
+ report_summary=outline.summary,
+ simulation_requirement=self.simulation_requirement,
+ section_title=section.title,
+ tools_description=self._get_tools_description(),
+ )
# 构建用户prompt - 每个已完成章节各传入最大4000字
if previous_sections:
@@ -1119,29 +1236,10 @@ class ReportAgent:
else:
previous_content = "(这是第一个章节)"
- user_prompt = f"""已完成的章节内容(请仔细阅读,避免重复):
-{previous_content}
-
-═══════════════════════════════════════════════════════════════
-【当前任务】撰写章节: {section.title}
-═══════════════════════════════════════════════════════════════
-
-【重要提醒】
-1. 仔细阅读上方已完成的章节,避免重复相同的内容!
-2. 开始前必须先调用工具获取模拟数据
-3. 请混合使用不同工具,不要只用一种
-4. 报告内容必须来自检索结果,不要使用自己的知识
-
-【⚠️ 格式警告 - 必须遵守】
-- ❌ 不要写任何标题(#、##、###、####都不行)
-- ❌ 不要写"{section.title}"作为开头
-- ✅ 章节标题由系统自动添加
-- ✅ 直接写正文,用**粗体**代替小节标题
-
-请开始:
-1. 首先思考(Thought)这个章节需要什么信息
-2. 然后调用工具(Action)获取模拟数据
-3. 收集足够信息后输出 Final Answer(纯正文,无任何标题)"""
+ user_prompt = SECTION_USER_PROMPT_TEMPLATE.format(
+ previous_content=previous_content,
+ section_title=section.title,
+ )
messages = [
{"role": "system", "content": system_prompt},
@@ -1211,7 +1309,11 @@ class ReportAgent:
unused_hint = f"(这些工具还未使用,推荐用一下他们: {', '.join(unused_tools)})" if unused_tools else ""
messages.append({
"role": "user",
- "content": f"【注意】你只调用了{tool_calls_count}次工具,至少需要{min_tool_calls}次。请再调用工具获取更多模拟数据,然后再输出 Final Answer。{unused_hint}"
+ "content": REACT_INSUFFICIENT_TOOLS_MSG.format(
+ tool_calls_count=tool_calls_count,
+ min_tool_calls=min_tool_calls,
+ unused_hint=unused_hint,
+ ),
})
continue
@@ -1235,7 +1337,10 @@ class ReportAgent:
messages.append({"role": "assistant", "content": response})
messages.append({
"role": "user",
- "content": f"工具调用次数已达上限({tool_calls_count}/{self.MAX_TOOL_CALLS_PER_SECTION}),不能再调用工具。请立即基于已获取的信息,以 \"Final Answer:\" 开头输出章节内容。"
+ "content": REACT_TOOL_LIMIT_MSG.format(
+ tool_calls_count=tool_calls_count,
+ max_tool_calls=self.MAX_TOOL_CALLS_PER_SECTION,
+ ),
})
continue
@@ -1275,22 +1380,19 @@ class ReportAgent:
unused_tools = all_tools - used_tools
unused_hint = ""
if unused_tools and tool_calls_count < self.MAX_TOOL_CALLS_PER_SECTION:
- unused_list = "、".join(unused_tools)
- unused_hint = f"\n💡 你还没有使用过: {unused_list},建议尝试不同工具获取多角度信息"
+ unused_hint = REACT_UNUSED_TOOLS_HINT.format(unused_list="、".join(unused_tools))
messages.append({"role": "assistant", "content": response})
messages.append({
"role": "user",
- "content": f"""Observation(检索结果):
-
-═══ 工具 {call['name']} 返回 ═══
-{result}
-
-═══════════════════════════════════════════════════════════════
-已调用工具 {tool_calls_count}/{self.MAX_TOOL_CALLS_PER_SECTION} 次(已用: {', '.join(used_tools)}){unused_hint}
-- 如果信息充分:以 "Final Answer:" 开头输出章节内容(必须引用上述原文)
-- 如果需要更多信息:调用一个工具继续检索
-═══════════════════════════════════════════════════════════════"""
+ "content": REACT_OBSERVATION_TEMPLATE.format(
+ tool_name=call["name"],
+ result=result,
+ tool_calls_count=tool_calls_count,
+ max_tool_calls=self.MAX_TOOL_CALLS_PER_SECTION,
+ used_tools_str=", ".join(used_tools),
+ unused_hint=unused_hint,
+ ),
})
continue
@@ -1304,7 +1406,11 @@ class ReportAgent:
messages.append({
"role": "user",
- "content": f"当前只调用了 {tool_calls_count} 次工具,至少需要 {min_tool_calls} 次。请调用工具获取模拟数据。{unused_hint}"
+ "content": REACT_INSUFFICIENT_TOOLS_MSG_ALT.format(
+ tool_calls_count=tool_calls_count,
+ min_tool_calls=min_tool_calls,
+ unused_hint=unused_hint,
+ ),
})
continue
@@ -1324,10 +1430,7 @@ class ReportAgent:
# 达到最大迭代次数,强制生成内容
logger.warning(f"章节 {section.title} 达到最大迭代次数,强制生成")
- messages.append({
- "role": "user",
- "content": "已达到工具调用限制,请直接输出 Final Answer: 并生成章节内容。"
- })
+ messages.append({"role": "user", "content": REACT_FORCE_FINAL_MSG})
response = self.llm.chat(
messages=messages,
@@ -1626,33 +1729,11 @@ class ReportAgent:
except Exception as e:
logger.warning(f"获取报告内容失败: {e}")
- # 构建系统提示
- system_prompt = f"""你是一个简洁高效的模拟预测助手。
-
-【背景】
-预测条件: {self.simulation_requirement}
-
-【已生成的分析报告】
-{report_content if report_content else "(暂无报告)"}
-
-【规则】
-1. 优先基于上述报告内容回答问题
-2. 直接回答问题,避免冗长的思考论述
-3. 仅在报告内容不足以回答时,才调用工具检索更多数据
-4. 回答要简洁、清晰、有条理
-
-【可用工具】(仅在需要时使用,最多调用1-2次)
-{self._get_tools_description()}
-
-【工具调用格式】
-
-{{"name": "工具名称", "parameters": {{"参数名": "参数值"}}}}
-
-
-【回答风格】
-- 简洁直接,不要长篇大论
-- 使用 > 格式引用关键内容
-- 优先给出结论,再解释原因"""
+ system_prompt = CHAT_SYSTEM_PROMPT_TEMPLATE.format(
+ simulation_requirement=self.simulation_requirement,
+ report_content=report_content if report_content else "(暂无报告)",
+ tools_description=self._get_tools_description(),
+ )
# 构建消息
messages = [{"role": "system", "content": system_prompt}]
@@ -1707,8 +1788,8 @@ class ReportAgent:
messages.append({"role": "assistant", "content": response})
observation = "\n".join([f"[{r['tool']}结果]\n{r['result']}" for r in tool_results])
messages.append({
- "role": "user",
- "content": observation + "\n\n请简洁回答问题。"
+ "role": "user",
+ "content": observation + CHAT_OBSERVATION_SUFFIX
})
# 达到最大迭代,获取最终响应