agents.py 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374
  1. """核心 Agent"""
  2. import json
  3. import os
  4. import hashlib
  5. from pathlib import Path
  6. from typing import Dict, Any, Optional, List
  7. from hello_agents import (
  8. HelloAgentsLLM,
  9. ReActAgent,
  10. ReflectionAgent,
  11. PlanAndSolveAgent
  12. )
  13. from hello_agents.tools import MCPTool, ToolRegistry, SearchTool
  14. from models import ColumnPlan, ReviewResult, ContentNode, ContentLevel
  15. from prompts import get_structure_requirements, get_react_writer_prompt, get_reflection_writer_prompts, get_planner_prompts
  16. from config import get_settings, get_word_count
  17. import re # Added for JSON parsing
  18. settings = get_settings()
  19. class LLMService:
  20. """LLM 服务单例"""
  21. _instance: Optional[HelloAgentsLLM] = None
  22. @classmethod
  23. def get_llm(cls) -> HelloAgentsLLM:
  24. """获取 LLM 实例(单例模式)"""
  25. if cls._instance is None:
  26. cls._instance = HelloAgentsLLM()
  27. print(f"▸ LLM服务初始化成功")
  28. print(f" 提供商: {cls._instance.provider}")
  29. print(f" 模型: {cls._instance.model}")
  30. return cls._instance
  31. class PlannerAgent:
  32. """
  33. 使用 PlanAndSolveAgent 模式
  34. PlanAndSolveAgent 将任务分解为子任务并逐步执行,非常适合专栏规划场景:
  35. 1. 分析主题(理解用户需求)
  36. 2. 规划子话题(分解任务)
  37. 3. 组织结构(逐步执行)
  38. 支持缓存机制,以主题为key缓存规划结果
  39. """
  40. def __init__(self, cache_dir: str = ".cache"):
  41. """
  42. 初始化规划 Agent
  43. Args:
  44. cache_dir: 缓存目录路径
  45. """
  46. self.llm = LLMService.get_llm()
  47. self.cache_dir = Path(cache_dir)
  48. self.cache_dir.mkdir(exist_ok=True)
  49. # 自定义 PlanAndSolve 提示词
  50. planner_prompts = {
  51. "planner": """
  52. 你是一位经验丰富的专栏策划专家。请将以下专栏主题分解为清晰的子话题规划步骤。
  53. 主题: {question}
  54. 请按以下格式输出规划步骤:
  55. ```python
  56. [
  57. "步骤1: 分析主题的核心概念和目标读者",
  58. "步骤2: 确定知识体系的整体框架",
  59. "步骤3: 规划2-4个子话题,确保逻辑递进",
  60. "步骤4: 为每个子话题设定学习目标和要点",
  61. "步骤5: 组装完整的专栏大纲"
  62. ]
  63. ```
  64. 不能超过10个步骤。
  65. """,
  66. "executor": """
  67. 你是专栏规划执行专家。请按照规划步骤执行专栏大纲的生成。
  68. # 原始主题: {question}
  69. # 规划步骤: {plan}
  70. # 已完成步骤: {history}
  71. # 当前步骤: {current_step}
  72. ▸️ **关键要求**:
  73. - 不能超过10个步骤。
  74. - 如果当前步骤是"步骤5: 组装完整的专栏大纲"或包含"组装"、"完整"、"大纲"等关键词,**必须**输出完整的 JSON 格式专栏大纲
  75. - 如果不是最后一步,请输出当前步骤的分析结果(文本格式)
  76. **最后一步的输出格式(必须是 JSON,不要添加任何其他文本)**:
  77. ```json
  78. {{
  79. "column_title": "专栏总标题",
  80. "column_description": "专栏简介(100-200字)",
  81. "target_audience": "目标读者群体",
  82. "topics": [
  83. {{
  84. "id": "topic_001",
  85. "title": "子话题标题",
  86. "description": "子话题简介(50-100字)",
  87. "estimated_words": 200,
  88. "key_points": ["要点1", "要点2", "要点3"],
  89. "prerequisites": ["前置知识1", "前置知识2"]
  90. }}
  91. ]
  92. }}
  93. ```
  94. **重要**:如果是最后一步,请直接输出 JSON,不要添加"当前步骤分析结果"等前缀文本。
  95. 请执行当前步骤:
  96. """
  97. }
  98. # 创建带缓存的 Executor 包装器
  99. from hello_agents.agents.plan_solve_agent import Executor
  100. class CachedExecutor(Executor):
  101. """带缓存的 Executor,缓存每个步骤的执行结果"""
  102. def __init__(self, llm_client, prompt_template, cache_dir, main_topic):
  103. super().__init__(llm_client, prompt_template)
  104. self.cache_dir = cache_dir
  105. self.main_topic = main_topic
  106. self.steps_cache_dir = cache_dir / "steps_cache"
  107. self.steps_cache_dir.mkdir(exist_ok=True)
  108. def _get_step_cache_key(self, step_index: int, step_content: str) -> Path:
  109. """生成步骤缓存文件路径"""
  110. # 使用主题 + 步骤索引 + 步骤内容的hash作为key
  111. step_hash = hashlib.md5(
  112. f"{self.main_topic}_{step_index}_{step_content}".encode('utf-8')
  113. ).hexdigest()
  114. return self.steps_cache_dir / f"step_{step_index}_{step_hash}.json"
  115. def _load_step_from_cache(self, step_index: int, step_content: str) -> Optional[str]:
  116. """从缓存加载步骤结果"""
  117. cache_file = self._get_step_cache_key(step_index, step_content)
  118. if not cache_file.exists():
  119. return None
  120. try:
  121. with open(cache_file, 'r', encoding='utf-8') as f:
  122. cache_data = json.load(f)
  123. # 验证缓存的主题和步骤是否匹配
  124. if (cache_data.get('topic') == self.main_topic and
  125. cache_data.get('step_index') == step_index and
  126. cache_data.get('step_content') == step_content):
  127. print(f" ▸ 从缓存加载步骤 {step_index} 的结果")
  128. return cache_data.get('result')
  129. except Exception as e:
  130. print(f" ▸️ 加载步骤缓存失败: {e}")
  131. return None
  132. def _save_step_to_cache(self, step_index: int, step_content: str, result: str):
  133. """保存步骤结果到缓存"""
  134. cache_file = self._get_step_cache_key(step_index, step_content)
  135. try:
  136. cache_data = {
  137. 'topic': self.main_topic,
  138. 'step_index': step_index,
  139. 'step_content': step_content,
  140. 'result': result
  141. }
  142. with open(cache_file, 'w', encoding='utf-8') as f:
  143. json.dump(cache_data, f, ensure_ascii=False, indent=2)
  144. except Exception as e:
  145. print(f" ▸️ 保存步骤缓存失败: {e}")
  146. def execute(self, question: str, plan: List[str], **kwargs) -> str:
  147. """按计划执行任务(带缓存)"""
  148. history = ""
  149. final_answer = ""
  150. print("\n--- 正在执行计划 ---")
  151. for i, step in enumerate(plan, 1):
  152. print(f"\n-> 正在执行步骤 {i}/{len(plan)}: {step}")
  153. # 尝试从缓存加载
  154. cached_result = self._load_step_from_cache(i, step)
  155. if cached_result:
  156. response_text = cached_result
  157. else:
  158. # 缓存未命中,执行步骤
  159. prompt = self.prompt_template.format(
  160. question=question,
  161. plan=plan,
  162. history=history if history else "无",
  163. current_step=step
  164. )
  165. messages = [{"role": "user", "content": prompt}]
  166. response_text = self.llm_client.invoke(messages, **kwargs) or ""
  167. # 保存到缓存
  168. self._save_step_to_cache(i, step, response_text)
  169. history += f"步骤 {i}: {step}\n结果: {response_text}\n\n"
  170. final_answer = response_text
  171. print(f"▸ 步骤 {i} 已完成,结果: {final_answer[:100] if len(final_answer) > 100 else final_answer}...")
  172. return final_answer
  173. # 创建 PlanAndSolveAgent,但替换 Executor
  174. self.agent = PlanAndSolveAgent(
  175. name="专栏规划专家",
  176. llm=self.llm,
  177. custom_prompts=planner_prompts
  178. )
  179. # 替换 Executor 为带缓存的版本
  180. cached_executor = CachedExecutor(
  181. llm_client=self.llm,
  182. prompt_template=planner_prompts["executor"],
  183. cache_dir=self.cache_dir,
  184. main_topic="" # 将在 plan_column 中设置
  185. )
  186. self.agent.executor = cached_executor
  187. def _get_cache_key(self, main_topic: str) -> str:
  188. """
  189. 生成缓存key(使用主题的hash值)
  190. Args:
  191. main_topic: 专栏主题
  192. Returns:
  193. 缓存文件名
  194. """
  195. # 使用主题的hash值作为文件名
  196. topic_hash = hashlib.md5(main_topic.encode('utf-8')).hexdigest()
  197. return f"plan_{topic_hash}.json"
  198. def _load_from_cache(self, main_topic: str) -> Optional[ColumnPlan]:
  199. """
  200. 从缓存加载规划结果
  201. Args:
  202. main_topic: 专栏主题
  203. Returns:
  204. ColumnPlan 实例,如果缓存不存在则返回 None
  205. """
  206. cache_file = self.cache_dir / self._get_cache_key(main_topic)
  207. if not cache_file.exists():
  208. return None
  209. try:
  210. with open(cache_file, 'r', encoding='utf-8') as f:
  211. cache_data = json.load(f)
  212. # 验证缓存的主题是否匹配
  213. if cache_data.get('topic') != main_topic:
  214. print(f"▸️ 缓存主题不匹配,忽略缓存")
  215. return None
  216. plan_data = cache_data.get('plan')
  217. if not plan_data:
  218. return None
  219. plan = ColumnPlan.from_dict(plan_data)
  220. print(f"▸ 从缓存加载规划结果")
  221. print(f" 缓存文件: {cache_file}")
  222. return plan
  223. except Exception as e:
  224. print(f"▸️ 加载缓存失败: {e}")
  225. return None
  226. def _save_to_cache(self, main_topic: str, plan: ColumnPlan):
  227. """
  228. 保存规划结果到缓存
  229. Args:
  230. main_topic: 专栏主题
  231. plan: ColumnPlan 实例
  232. """
  233. cache_file = self.cache_dir / self._get_cache_key(main_topic)
  234. try:
  235. cache_data = {
  236. 'topic': main_topic,
  237. 'plan': plan.to_dict(),
  238. 'cached_at': str(Path(__file__).stat().st_mtime) # 简单的缓存时间戳
  239. }
  240. with open(cache_file, 'w', encoding='utf-8') as f:
  241. json.dump(cache_data, f, ensure_ascii=False, indent=2)
  242. print(f"▸ 规划结果已保存到缓存: {cache_file}")
  243. except Exception as e:
  244. print(f"▸️ 保存缓存失败: {e}")
  245. def plan_column(self, main_topic: str, use_cache: bool = True) -> ColumnPlan:
  246. """
  247. 规划专栏大纲
  248. Args:
  249. main_topic: 专栏主题
  250. use_cache: 是否使用缓存(默认True)
  251. Returns:
  252. ColumnPlan 实例
  253. """
  254. # 尝试从缓存加载
  255. if use_cache:
  256. cached_plan = self._load_from_cache(main_topic)
  257. if cached_plan:
  258. print(f" 专栏标题: {cached_plan.column_title}")
  259. print(f" 话题数量: {cached_plan.get_topic_count()}")
  260. return cached_plan
  261. # 缓存未命中,调用 LLM 进行规划
  262. print(f"\n▸ PlanAndSolve Agent 开始规划专栏...")
  263. print(f" 使用模式: 任务分解 → 逐步执行")
  264. print(f" 主题: {main_topic}")
  265. # 更新 Executor 的主题(用于缓存key)
  266. if hasattr(self.agent.executor, 'main_topic'):
  267. self.agent.executor.main_topic = main_topic
  268. response = self.agent.run(main_topic)
  269. # 解析 JSON 响应
  270. plan_data = self._extract_json(response)
  271. plan = ColumnPlan.from_dict(plan_data)
  272. print(f"▸ 规划完成")
  273. print(f" 专栏标题: {plan.column_title}")
  274. print(f" 话题数量: {plan.get_topic_count()}")
  275. # 保存到缓存
  276. if use_cache:
  277. self._save_to_cache(main_topic, plan)
  278. return plan
  279. def _extract_json(self, response: str) -> Dict[str, Any]:
  280. """从响应中提取 JSON(增强版,支持从历史记录中提取)"""
  281. try:
  282. # 方法1: 直接是 JSON
  283. if response.strip().startswith('{'):
  284. return json.loads(response.strip())
  285. # 方法2: Markdown 代码块中的 JSON
  286. if "```json" in response:
  287. json_start = response.find("```json") + 7
  288. json_end = response.find("```", json_start)
  289. if json_end != -1:
  290. json_str = response[json_start:json_end].strip()
  291. return json.loads(json_str)
  292. # 方法3: 普通代码块中的 JSON
  293. if "```" in response:
  294. json_start = response.find("```") + 3
  295. json_end = response.find("```", json_start)
  296. if json_end != -1:
  297. json_str = response[json_start:json_end].strip()
  298. # 移除可能的语言标识符
  299. if json_str.startswith("json"):
  300. json_str = json_str[4:].strip()
  301. if json_str.startswith('{'):
  302. return json.loads(json_str)
  303. # 方法4: 查找所有可能的 JSON 对象(从最大的开始尝试)
  304. # 找到所有 { 和 } 的位置
  305. brace_positions = []
  306. for i, char in enumerate(response):
  307. if char == '{':
  308. brace_positions.append(('{', i))
  309. elif char == '}':
  310. brace_positions.append(('}', i))
  311. # 尝试从最后一个 { 到最后一个 } 提取 JSON
  312. if brace_positions:
  313. first_open = next((i for char, i in brace_positions if char == '{'), None)
  314. last_close = next((i for char, i in reversed(brace_positions) if char == '}'), None)
  315. if first_open is not None and last_close is not None and last_close > first_open:
  316. # 尝试提取完整的 JSON
  317. potential_json = response[first_open:last_close + 1]
  318. try:
  319. return json.loads(potential_json)
  320. except json.JSONDecodeError:
  321. pass
  322. # 如果失败,尝试找到包含 "column_title" 或 "topics" 的 JSON(专栏规划的特征字段)
  323. # 使用正则表达式找到包含这些字段的 JSON 块
  324. import re
  325. json_pattern = r'\{[^{}]*(?:"column_title"|"topics")[^{}]*\{[^{}]*\}[^{}]*\}'
  326. matches = re.finditer(json_pattern, response, re.DOTALL)
  327. for match in matches:
  328. try:
  329. return json.loads(match.group(0))
  330. except json.JSONDecodeError:
  331. continue
  332. # 更宽松的匹配:找到包含 "column_title" 的 JSON
  333. column_title_match = re.search(r'\{[^{}]*"column_title"[^{}]*\{[^{}]*"topics"[^{}]*\[.*?\][^{}]*\}[^{}]*\}', response, re.DOTALL)
  334. if column_title_match:
  335. try:
  336. return json.loads(column_title_match.group(0))
  337. except json.JSONDecodeError:
  338. pass
  339. # 如果都失败了,抛出错误
  340. raise ValueError("响应中未找到有效的 JSON 数据")
  341. except json.JSONDecodeError as e:
  342. print(f"▸️ JSON 解析失败: {e}")
  343. print(f" 响应内容(前1000字符): {response[:1000]}...")
  344. # 尝试从历史记录中查找 JSON(如果响应中包含历史信息)
  345. if "步骤" in response and "结果" in response:
  346. print(" 尝试从历史记录中提取 JSON...")
  347. # 查找所有包含 JSON 的步骤结果
  348. import re
  349. json_matches = re.findall(r'```json\s*(\{.*?\})\s*```', response, re.DOTALL)
  350. if not json_matches:
  351. json_matches = re.findall(r'(\{"column_title".*?"topics".*?\})', response, re.DOTALL)
  352. for json_str in json_matches:
  353. try:
  354. return json.loads(json_str)
  355. except json.JSONDecodeError:
  356. continue
  357. raise ValueError(f"响应中未找到有效的 JSON 数据: {str(e)}")
  358. except Exception as e:
  359. print(f"▸️ JSON 提取失败: {e}")
  360. print(f" 响应内容(前500字符): {response[:500]}...")
  361. raise
  362. def improved_parse_output(self, text: str):
  363. """
  364. 改进的解析方法,支持更多格式和边界情况
  365. Args:
  366. self: Agent 实例(当作为方法绑定时需要)
  367. text: LLM 的原始响应文本
  368. Returns:
  369. (thought, action) 元组
  370. """
  371. if not text or not text.strip():
  372. print("▸️ 警告: LLM 返回了空响应")
  373. return None, None
  374. # 尝试多种格式解析 Thought
  375. thought = None
  376. thought_patterns = [
  377. r"Thought:\s*(.*?)(?=\nAction:|\nFinish:|$)", # 标准格式
  378. r"思考:\s*(.*?)(?=\n行动:|\n完成:|$)", # 中文格式
  379. r"▸\s*(.*?)(?=\n▸|\n▸|$)", # emoji格式
  380. ]
  381. thought_end_pos = 0
  382. for pattern in thought_patterns:
  383. match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
  384. if match:
  385. thought = match.group(1).strip()
  386. if thought:
  387. thought_end_pos = match.end()
  388. break
  389. # 尝试多种格式解析 Action
  390. action = None
  391. action_patterns = [
  392. r"Action:\s*(.*?)(?=\nThought:|\nObservation:|\nFinish:|$)", # 标准格式
  393. r"行动:\s*(.*?)(?=\n思考:|\n观察:|\n完成:|$)", # 中文格式
  394. r"▸\s*(.*?)(?=\n▸|\n▸|\n▸|$)", # emoji格式
  395. r"Finish\[(.*?)\]", # Finish格式(可能没有Action前缀)
  396. ]
  397. for pattern in action_patterns:
  398. match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
  399. if match:
  400. action = match.group(1).strip()
  401. if action:
  402. # 如果是 Finish 格式,需要加上 Finish 前缀
  403. if pattern == r"Finish\[(.*?)\]":
  404. action = f"Finish[{action}]"
  405. break
  406. # 如果仍然没有找到 Action,尝试查找 Finish 关键字(可能格式不标准)
  407. if not action:
  408. finish_patterns = [
  409. r"Finish\s*\[(.*?)\]",
  410. r"完成\s*\[(.*?)\]",
  411. r"最终答案:\s*(.*?)(?=\n|$)",
  412. ]
  413. for pattern in finish_patterns:
  414. match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
  415. if match:
  416. content = match.group(1).strip()
  417. if content:
  418. action = f"Finish[{content}]"
  419. break
  420. # 如果仍然没有找到 Action,检查 Thought 之后是否有正文内容
  421. # 或者即使没有 Thought,也检查是否有直接的内容(JSON 或正文)
  422. if not action:
  423. # 首先尝试从整个文本中提取 JSON(因为 Thought 的正则可能包含了后续内容)
  424. # 查找第一个 { 到最后一个 } 之间的内容(可能是 JSON)
  425. json_match = None
  426. brace_start = text.find('{')
  427. if brace_start != -1:
  428. # 找到最后一个 }
  429. brace_end = text.rfind('}')
  430. if brace_end > brace_start:
  431. potential_json = text[brace_start:brace_end + 1]
  432. # 检查是否包含 content 字段
  433. if '"content"' in potential_json or "'content'" in potential_json:
  434. json_match = re.search(r'\{.*?"content".*?\}', potential_json, re.DOTALL)
  435. # 如果没有找到 JSON,检查 Thought 之后或 Thought 内容中的其他内容
  436. if thought:
  437. remaining_text = text[thought_end_pos:].strip()
  438. if not remaining_text:
  439. # Thought 内容可能包含了完整的正文
  440. remaining_text = thought
  441. else:
  442. # 没有 Thought,直接检查整个文本
  443. remaining_text = text.strip()
  444. # 移除可能的 Action: 或 Finish: 前缀(如果格式不标准)
  445. remaining_text = re.sub(r'^(Action|Finish|行动|完成)[::]\s*', '', remaining_text, flags=re.IGNORECASE)
  446. if remaining_text or json_match:
  447. # 如果找到了 JSON,使用 JSON 内容
  448. if json_match:
  449. remaining_text = json_match.group(0)
  450. has_json = True
  451. json_str = remaining_text
  452. # 检查 JSON 是否完整(有配对的括号)
  453. open_braces = json_str.count('{')
  454. close_braces = json_str.count('}')
  455. json_complete = (open_braces == close_braces) and open_braces > 0
  456. else:
  457. # 检查是否包含 JSON 结构(可能是完整的文章内容)
  458. json_match = re.search(r'\{.*?"content".*?\}', remaining_text, re.DOTALL)
  459. has_json = bool(json_match)
  460. # 如果找到 JSON,检查是否完整(有配对的括号)
  461. json_complete = False
  462. if has_json:
  463. json_str = json_match.group(0)
  464. # 简单检查:大括号是否配对
  465. open_braces = json_str.count('{')
  466. close_braces = json_str.count('}')
  467. json_complete = (open_braces == close_braces) and open_braces > 0
  468. has_content_field = bool(re.search(r'"content"\s*:\s*"', remaining_text, re.DOTALL))
  469. # 检查是否有明显的结尾标记
  470. has_ending = bool(re.search(r'(总结|结论|结语|小结|综上所述|总之|最后|end|conclusion)', remaining_text[-500:], re.IGNORECASE))
  471. # 检查是否有"未完待续"的标记(表示还想继续写)
  472. has_continuation_marker = bool(re.search(r'(未完待续|待续|继续|to be continued|未完|待补充)', remaining_text, re.IGNORECASE))
  473. # 检查内容长度(如果超过一定长度,可能是完整内容)
  474. content_length = len(remaining_text)
  475. is_substantial = content_length > 200 # 至少200字符
  476. # 判断是否应该自动添加 Finish
  477. # 优先级:1. 完整的 JSON 结构 > 2. 有结尾标记(即使内容稍短)> 3. 内容足够长且没有未完标记
  478. is_complete = False
  479. completion_reason = []
  480. if json_complete:
  481. is_complete = True
  482. completion_reason.append("完整的 JSON 结构")
  483. elif has_ending:
  484. # 有结尾标记,即使内容稍短也认为是完整的(模型已经表达了完成意图)
  485. is_complete = True
  486. if is_substantial:
  487. completion_reason.append("有结尾标记且内容足够长")
  488. else:
  489. completion_reason.append("有结尾标记(模型表达了完成意图)")
  490. elif is_substantial and not has_continuation_marker:
  491. # 内容足够长且没有未完标记,可能是完整内容
  492. is_complete = True
  493. completion_reason.append("内容足够长且无未完标记")
  494. if is_complete:
  495. print(f"▸ 检测到完整正文内容(长度: {content_length} 字符),自动添加 Finish 前缀")
  496. print(f" - 判断依据: {', '.join(completion_reason)}")
  497. action = f"Finish[{remaining_text}]"
  498. else:
  499. # 内容不完整,可能还想继续写
  500. print(f"▸️ 检测到部分正文内容(长度: {content_length} 字符),但可能未完成")
  501. if has_continuation_marker:
  502. print(f" - 检测到'未完待续'标记,继续循环让模型完成写作")
  503. elif not is_substantial:
  504. print(f" - 内容长度不足,继续循环让模型完成写作")
  505. else:
  506. print(f" - 未检测到明确的完成标记,继续循环让模型完成写作")
  507. # 不设置 action,让循环继续
  508. return thought, None
  509. # 调试信息
  510. if not action:
  511. print(f"▸️ 警告: 未能解析出 Action")
  512. print(f" 响应内容(前500字符): {text[:500]}")
  513. print(f" 已解析的 Thought: {thought[:100] if thought else 'None'}...")
  514. return thought, action
  515. class ReActAgentWrapper:
  516. """
  517. ReActAgent 包装器,用于捕获历史信息和处理错误
  518. """
  519. def __init__(self, agent: ReActAgent):
  520. self.agent = agent
  521. self.last_history = [] # 保存最后一次运行的历史
  522. self.last_response = None # run() 方法的返回值(通常是 final_answer)
  523. self.last_raw_responses = [] # 保存所有原始 LLM 响应,用于调试
  524. def run(self, question: str, max_retries: int = 2):
  525. """
  526. 运行 Agent 并捕获历史信息
  527. Args:
  528. question: 问题
  529. max_retries: 最大重试次数(当解析失败时)
  530. """
  531. try:
  532. # 清空上次的原始响应
  533. self.last_raw_responses = []
  534. # 尝试访问 agent 的 history 属性(如果存在)
  535. if hasattr(self.agent, 'current_history'):
  536. original_history = self.agent.current_history.copy() if self.agent.current_history else []
  537. elif hasattr(self.agent, 'history'):
  538. original_history = self.agent.history.copy() if self.agent.history else []
  539. else:
  540. original_history = []
  541. # 如果 agent 有 _parse_output 方法,保存原始方法并替换为改进版本
  542. original_parse = None
  543. original_invoke = None
  544. if hasattr(self.agent, '_parse_output'):
  545. original_parse = self.agent._parse_output
  546. # 使用改进的解析方法(绑定到实例)
  547. import types
  548. self.agent._parse_output = types.MethodType(improved_parse_output, self.agent)
  549. # 拦截 LLM 调用以捕获原始响应
  550. if hasattr(self.agent, 'llm') and hasattr(self.agent.llm, 'invoke'):
  551. original_invoke = self.agent.llm.invoke
  552. def wrapped_invoke(messages, **kwargs):
  553. """包装 LLM invoke 方法以捕获原始响应"""
  554. response = original_invoke(messages, **kwargs)
  555. if response:
  556. self.last_raw_responses.append(response)
  557. return response
  558. self.agent.llm.invoke = wrapped_invoke
  559. try:
  560. response = self.agent.run(question)
  561. self.last_response = response
  562. # 尝试获取最终的历史信息
  563. if hasattr(self.agent, 'current_history'):
  564. self.last_history = self.agent.current_history.copy() if self.agent.current_history else []
  565. elif hasattr(self.agent, 'history'):
  566. self.last_history = self.agent.history.copy() if self.agent.history else []
  567. else:
  568. self.last_history = original_history
  569. return response
  570. finally:
  571. # 恢复原始方法
  572. if original_parse:
  573. self.agent._parse_output = original_parse
  574. if original_invoke and hasattr(self.agent, 'llm'):
  575. self.agent.llm.invoke = original_invoke
  576. except Exception as e:
  577. # 即使出错也尝试保存历史
  578. if hasattr(self.agent, 'current_history'):
  579. self.last_history = self.agent.current_history.copy() if self.agent.current_history else []
  580. elif hasattr(self.agent, 'history'):
  581. self.last_history = self.agent.history.copy() if self.agent.history else []
  582. print(f"▸️ ReActAgentWrapper 捕获到异常: {e}")
  583. raise
  584. class WriterAgent:
  585. """
  586. 写作 Agent - 使用 ReActAgent 模式
  587. ReActAgent 结合推理(Reasoning)和行动(Acting),非常适合需要工具调用的写作场景:
  588. 1. 分析写作需求(推理)
  589. 2. 决定是否需要搜索(推理)
  590. 3. 调用搜索工具(行动)
  591. 4. 整合信息写作(行动)
  592. """
  593. def __init__(self, enable_search: bool = True):
  594. """
  595. 初始化写作 Agent
  596. Args:
  597. enable_search: 是否启用搜索功能
  598. """
  599. self.llm = LLMService.get_llm()
  600. self.enable_search = enable_search
  601. # 创建工具注册表
  602. self.tool_registry = ToolRegistry()
  603. # 添加搜索工具(如果启用)
  604. if enable_search:
  605. self._setup_search_tool()
  606. # 自定义 ReAct 提示词(参考示例代码的简洁格式)
  607. react_prompt = get_react_writer_prompt() # 从 prompts.py 获取
  608. # 创建 ReActAgent(将在包装器中替换解析方法)
  609. react_agent = ReActAgent(
  610. name="内容创作专家",
  611. llm=self.llm,
  612. tool_registry=self.tool_registry,
  613. custom_prompt=react_prompt,
  614. max_steps=10 # 增加到 10 步,给 Agent 更多机会完成任务
  615. )
  616. self.agent = ReActAgentWrapper(react_agent)
  617. def _setup_search_tool(self):
  618. """设置搜索工具(使用 SearchTool 和 MCPTool)"""
  619. settings = get_settings()
  620. # 保存 search_tool 实例供 wrappers 使用
  621. self.search_tool = None
  622. # 1. 初始化内置 SearchTool
  623. try:
  624. # 检查是否配置了搜索 API
  625. if settings.tavily_api_key or settings.serpapi_api_key:
  626. self.search_tool = SearchTool(
  627. tavily_key=settings.tavily_api_key,
  628. serpapi_key=settings.serpapi_api_key
  629. )
  630. print("▸ SearchTool (内置) 已初始化")
  631. else:
  632. print("▸️ 未配置搜索 API Key (Tavily/SerpApi),跳过 SearchTool 初始化")
  633. except Exception as e:
  634. print(f"▸️ 初始化 SearchTool 失败: {e}")
  635. # 2. 注册 wrapper 函数 (如果 search_tool 可用)
  636. if self.search_tool:
  637. self._register_search_wrappers()
  638. # 3. 注册 GitHub MCPTool
  639. try:
  640. # 检查是否有 GitHub Token (通常在环境变量 GITHUB_PERSONAL_ACCESS_TOKEN)
  641. if os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN"):
  642. github_tool = MCPTool(
  643. name="github",
  644. description="GitHub 操作工具,支持搜索仓库、查看代码等",
  645. server_command=["npx", "-y", "@modelcontextprotocol/server-github"],
  646. auto_expand=True
  647. )
  648. self.tool_registry.register_tool(github_tool)
  649. print("▸ GitHub MCPTool 已注册")
  650. else:
  651. print("▸️ 未配置 GITHUB_PERSONAL_ACCESS_TOKEN,跳过 GitHub MCPTool 注册")
  652. except Exception as e:
  653. print(f"▸️ 注册 GitHub MCPTool 失败: {e}")
  654. def _register_search_wrappers(self):
  655. """注册适配 Prompt 的搜索函数 wrappers"""
  656. def web_search(query: str) -> str:
  657. """通用网页搜索,获取最新资讯和资料"""
  658. # SearchTool.run 接受 dict 参数
  659. return str(self.search_tool.run({"query": query}))
  660. def search_recent_info(topic: str) -> str:
  661. """搜索最新信息和动态"""
  662. return str(self.search_tool.run({"query": f"{topic} latest info"}))
  663. def search_code_examples(technology: str, task: str) -> str:
  664. """搜索代码示例和教程"""
  665. return str(self.search_tool.run({"query": f"{technology} {task} code examples tutorial"}))
  666. def verify_facts(statement: str) -> str:
  667. """验证事实准确性"""
  668. return str(self.search_tool.run({"query": f"verify fact: {statement}"}))
  669. self.tool_registry.register_function("web_search", "通用网页搜索,获取最新资讯和资料", web_search)
  670. self.tool_registry.register_function("search_recent_info", "搜索最新信息和动态", search_recent_info)
  671. self.tool_registry.register_function("search_code_examples", "搜索代码示例和教程", search_code_examples)
  672. self.tool_registry.register_function("verify_facts", "验证事实准确性", verify_facts)
  673. print("▸ 搜索函数 wrappers 已注册")
  674. def generate_content(
  675. self,
  676. node: ContentNode,
  677. context: Dict[str, Any],
  678. level: int,
  679. additional_requirements: str = ""
  680. ) -> Dict[str, Any]:
  681. """
  682. 生成内容(使用 ReAct 模式)
  683. Args:
  684. node: 当前节点
  685. context: 写作上下文
  686. level: 当前层级
  687. additional_requirements: 额外要求
  688. Returns:
  689. 生成的内容数据
  690. """
  691. structure_requirements = get_structure_requirements(level)
  692. word_count = get_word_count(level)
  693. # 构建写作任务描述(简化格式,参考示例代码)
  694. task_description = f"""
  695. 请撰写一篇技术专栏文章。
  696. 层级: Level {level}/3
  697. 话题: {node.title}
  698. 描述: {node.description}
  699. 要求字数: {word_count} 字(允许误差±10%)
  700. 上下文信息:
  701. {json.dumps(context, ensure_ascii=False, indent=2)}
  702. 结构要求:
  703. {structure_requirements}
  704. 额外要求:
  705. {additional_requirements if additional_requirements else "无"}
  706. 重要提示:
  707. - 完成写作后,必须使用 `\n\nFinish[JSON内容]` 格式输出结果
  708. - JSON 中的 `level` 字段必须是 {level}
  709. - `content` 字段必须包含完整的文章正文(Markdown格式)
  710. - 文章必须包含:引言、主体内容(3-5个小节)、实践案例、总结
  711. """
  712. try:
  713. response = self.agent.run(task_description)
  714. # 调试:打印真正的原始 LLM 响应(最后一次的响应)
  715. print(f"\n{'='*70}")
  716. print("▸ ReActAgent 原始 LLM 响应:")
  717. print(f"{'='*70}")
  718. if self.agent.last_raw_responses:
  719. # 打印最后一次的原始响应(通常是包含 Finish[...] 的那次)
  720. last_raw = self.agent.last_raw_responses[-1]
  721. print(last_raw)
  722. # print(last_raw[:2000] if len(last_raw) > 2000 else last_raw)
  723. # if len(last_raw) > 2000:
  724. # print(f"\n... (响应过长,已截断,总长度: {len(last_raw)} 字符)")
  725. else:
  726. print("▸️ 未捕获到原始响应")
  727. print(f"{'='*70}\n")
  728. # 打印 run() 方法的返回值(通常是 final_answer)
  729. print(f"▸ ReActAgent.run() 返回值:")
  730. print(f" {response[:500] if response and len(response) > 500 else response}")
  731. print()
  732. # 检查响应是否有效
  733. # 注意:即使 response 为空或错误,也要检查是否有原始响应可以提取
  734. if not response or (isinstance(response, str) and not response.strip()):
  735. print("▸️ ReActAgent 返回了空响应或空白响应")
  736. print(f" 已收集的历史信息: {len(self.agent.last_history)} 条")
  737. # 尝试从最后一次原始响应中提取内容
  738. if self.agent.last_raw_responses:
  739. last_raw = self.agent.last_raw_responses[-1]
  740. print(f" 尝试从最后一次原始响应中提取内容(长度: {len(last_raw)} 字符)...")
  741. # 尝试直接提取 JSON
  742. try:
  743. content_data = self._extract_json(last_raw)
  744. # 验证提取的 JSON 是否包含必需的字段
  745. if not isinstance(content_data, dict):
  746. raise ValueError("提取的内容不是字典格式")
  747. if 'content' not in content_data:
  748. print(f" ▸️ 提取的 JSON 缺少 'content' 字段")
  749. print(f" 可用字段: {list(content_data.keys())}")
  750. raise ValueError("提取的 JSON 缺少 'content' 字段")
  751. print("▸ 成功从原始响应中提取到内容")
  752. return content_data
  753. except Exception as e:
  754. print(f" ▸️ 从原始响应提取失败: {e}")
  755. # 如果提取失败,使用 fallback
  756. return self._generate_content_with_history(
  757. node, context, level, structure_requirements, word_count,
  758. self.agent.last_history, task_description
  759. )
  760. # 检查是否是错误消息
  761. if "无法在限定步数内完成" in response or "抱歉" in response or "流程终止" in response:
  762. print("▸️ ReActAgent 达到最大步数限制或无法完成任务")
  763. print(f" 已收集的历史信息: {len(self.agent.last_history)} 条")
  764. # 即使返回错误消息,也尝试从最后一次原始响应中提取内容
  765. if self.agent.last_raw_responses:
  766. last_raw = self.agent.last_raw_responses[-1]
  767. print(f" 尝试从最后一次原始响应中提取内容(长度: {len(last_raw)} 字符)...")
  768. try:
  769. content_data = self._extract_json(last_raw)
  770. # 验证提取的 JSON 是否包含必需的字段
  771. if not isinstance(content_data, dict):
  772. raise ValueError("提取的内容不是字典格式")
  773. if 'content' not in content_data:
  774. print(f" ▸️ 提取的 JSON 缺少 'content' 字段")
  775. print(f" 可用字段: {list(content_data.keys())}")
  776. raise ValueError("提取的 JSON 缺少 'content' 字段")
  777. print("▸ 成功从原始响应中提取到内容(尽管 ReActAgent 返回了错误消息)")
  778. return content_data
  779. except Exception as e:
  780. print(f" ▸️ 从原始响应提取失败: {e}")
  781. # 如果提取失败,基于历史信息生成内容
  782. return self._generate_content_with_history(
  783. node, context, level, structure_requirements, word_count,
  784. self.agent.last_history, task_description
  785. )
  786. # 如果 response 是 "JSON内容" 这样的占位符,从原始响应中提取
  787. if response.strip() in ["JSON内容", "JSON", "内容"]:
  788. print(f"▸️ ReActAgent 返回了占位符 '{response}',尝试从原始响应中提取...")
  789. if self.agent.last_raw_responses:
  790. last_raw = self.agent.last_raw_responses[-1]
  791. print(f" 从最后一次原始响应中提取(长度: {len(last_raw)} 字符)...")
  792. try:
  793. content_data = self._extract_json(last_raw)
  794. if isinstance(content_data, dict) and 'content' in content_data:
  795. print("▸ 成功从原始响应中提取到内容")
  796. return content_data
  797. except Exception as e:
  798. print(f" ▸️ 从原始响应提取失败: {e}")
  799. content_data = self._extract_json(response)
  800. # 验证提取的 JSON 是否包含必需的字段
  801. if not isinstance(content_data, dict):
  802. raise ValueError(f"提取的内容不是字典格式: {type(content_data)}")
  803. if 'content' not in content_data:
  804. print(f"▸️ 提取的 JSON 缺少 'content' 字段")
  805. print(f" 可用字段: {list(content_data.keys())}")
  806. print(f" 响应内容(前500字符): {response[:500]}")
  807. # 如果从 response 提取失败,尝试从原始响应中提取
  808. if self.agent.last_raw_responses:
  809. last_raw = self.agent.last_raw_responses[-1]
  810. print(f" 尝试从最后一次原始响应中提取(长度: {len(last_raw)} 字符)...")
  811. try:
  812. content_data = self._extract_json(last_raw)
  813. if isinstance(content_data, dict) and 'content' in content_data:
  814. print("▸ 成功从原始响应中提取到内容")
  815. return content_data
  816. except Exception as e:
  817. print(f" ▸️ 从原始响应提取失败: {e}")
  818. raise ValueError("提取的 JSON 缺少 'content' 字段")
  819. return content_data
  820. except Exception as e:
  821. print(f"▸️ ReActAgent 执行失败: {e}")
  822. import traceback
  823. traceback.print_exc()
  824. print(f" 已收集的历史信息: {len(self.agent.last_history)} 条")
  825. print(" 尝试基于历史信息生成内容...")
  826. return self._generate_content_with_history(
  827. node, context, level, structure_requirements, word_count,
  828. self.agent.last_history, task_description
  829. )
  830. def _generate_content_with_history(
  831. self,
  832. node: ContentNode,
  833. context: Dict[str, Any],
  834. level: int,
  835. structure_requirements: str,
  836. word_count: int,
  837. history: List[str],
  838. original_task: str
  839. ) -> Dict[str, Any]:
  840. """
  841. 当 ReActAgent 失败时,基于历史信息使用 SimpleAgent 生成内容
  842. Args:
  843. history: ReActAgent 收集的历史信息(Thought、Action、Observation)
  844. """
  845. from hello_agents import SimpleAgent
  846. fallback_agent = SimpleAgent(
  847. name="内容创作专家(备用)",
  848. llm=self.llm,
  849. system_prompt="你是一位专业的内容创作者,擅长撰写技术专栏文章。"
  850. )
  851. # 构建包含历史信息的任务描述
  852. history_summary = ""
  853. if history:
  854. history_summary = "\n\n## 已撰写的部分历史:\n"
  855. for i, item in enumerate(history[-10:], 1): # 只取最后10条历史
  856. history_summary += f"{i}. {item}\n"
  857. history_summary += "\n请基于以上信息继续完成写作任务。\n"
  858. task = f"""
  859. 请撰写一篇技术专栏文章。
  860. 话题: {node.title}
  861. 描述: {node.description}
  862. 要求字数: {word_count} 字
  863. 结构要求:
  864. {structure_requirements}
  865. {history_summary}
  866. 请直接输出 JSON 格式的内容:
  867. {{
  868. "title": "{node.title}",
  869. "level": {level},
  870. "content": "完整的文章正文(markdown格式,包含引言、主体、案例、总结)",
  871. "word_count": 实际字数,
  872. "needs_expansion": false,
  873. "subsections": [],
  874. "metadata": {{}}
  875. }}
  876. """
  877. print(f"▸ 使用 SimpleAgent 基于历史信息生成内容...")
  878. response = fallback_agent.run(task)
  879. return self._extract_json(response)
  880. def revise_content(
  881. self,
  882. original_content: str,
  883. review_result: ReviewResult,
  884. level: int
  885. ) -> Dict[str, Any]:
  886. """
  887. 根据评审意见修改内容
  888. Args:
  889. original_content: 原始内容
  890. review_result: 评审结果
  891. level: 层级
  892. Returns:
  893. 修改后的内容数据
  894. """
  895. # 构建修改任务
  896. task_description = f"""
  897. ## 修改任务
  898. **原始内容**:
  899. {original_content[:500]}...
  900. **评审分数**: {review_result.score}/100
  901. **评审等级**: {review_result.grade}
  902. **主要问题**:
  903. {json.dumps(review_result.detailed_feedback.get('issues', [])[:3], ensure_ascii=False, indent=2)}
  904. **修改建议**:
  905. {json.dumps(review_result.revision_plan.get('priority_changes', []), ensure_ascii=False, indent=2)}
  906. 请使用 ReAct 模式完成修改:
  907. 1. 思考评审意见的核心要求
  908. 2. 决定是否需要搜索新信息
  909. 3. 修改内容
  910. 4. 使用 Finish[修改后的JSON内容] 输出结果
  911. """
  912. response = self.agent.run(task_description)
  913. revised_data = self._extract_json(response)
  914. return revised_data
  915. def _extract_json(self, response: str) -> Dict[str, Any]:
  916. """
  917. 从响应中提取 JSON(支持多种格式,包括 Finish[...] 格式)
  918. 增强的 JSON 解析,能够处理包含复杂字符串的 JSON
  919. """
  920. import re
  921. import json.encoder
  922. def extract_json_with_retry(json_str: str) -> Dict[str, Any]:
  923. """尝试多种方式解析 JSON"""
  924. # 方法1: 直接解析
  925. try:
  926. return json.loads(json_str)
  927. except json.JSONDecodeError:
  928. pass
  929. # 方法2: 尝试修复常见的 JSON 问题
  930. # 修复未转义的换行符
  931. fixed = json_str.replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
  932. try:
  933. return json.loads(fixed)
  934. except json.JSONDecodeError:
  935. pass
  936. # 方法3: 尝试提取并重新构建 JSON
  937. # 提取各个字段
  938. title_match = re.search(r'"title"\s*:\s*"([^"]*)"', json_str)
  939. level_match = re.search(r'"level"\s*:\s*(\d+)', json_str)
  940. word_count_match = re.search(r'"word_count"\s*:\s*(\d+)', json_str)
  941. needs_expansion_match = re.search(r'"needs_expansion"\s*:\s*(true|false)', json_str)
  942. # 提取 content(可能跨多行)
  943. content_match = re.search(r'"content"\s*:\s*"(.*?)"(?=\s*[,}])', json_str, re.DOTALL)
  944. if not content_match:
  945. # 尝试另一种格式
  946. content_match = re.search(r'"content"\s*:\s*"([^"]*(?:\\.[^"]*)*)"', json_str, re.DOTALL)
  947. result = {}
  948. if title_match:
  949. result['title'] = title_match.group(1)
  950. if level_match:
  951. result['level'] = int(level_match.group(1))
  952. if content_match:
  953. # 处理转义字符
  954. content = content_match.group(1)
  955. content = content.replace('\\n', '\n').replace('\\r', '\r').replace('\\t', '\t')
  956. result['content'] = content
  957. if word_count_match:
  958. result['word_count'] = int(word_count_match.group(1))
  959. else:
  960. result['word_count'] = len(result.get('content', ''))
  961. if needs_expansion_match:
  962. result['needs_expansion'] = needs_expansion_match.group(1) == 'true'
  963. else:
  964. result['needs_expansion'] = False
  965. result['subsections'] = []
  966. result['metadata'] = {}
  967. return result
  968. try:
  969. # 方法1: 尝试从 Finish[...] 格式中提取(ReAct 标准格式)
  970. finish_match = re.search(r"Finish\[(.*?)\]", response, re.DOTALL)
  971. if finish_match:
  972. finish_content = finish_match.group(1).strip()
  973. print(f"▸ 找到 Finish 格式,内容长度: {len(finish_content)}")
  974. return extract_json_with_retry(finish_content)
  975. # 方法2: 直接是 JSON 对象
  976. if response.strip().startswith('{'):
  977. return extract_json_with_retry(response.strip())
  978. # 方法3: Markdown 代码块中的 JSON
  979. if "```json" in response:
  980. json_start = response.find("```json") + 7
  981. json_end = response.find("```", json_start)
  982. json_str = response[json_start:json_end].strip()
  983. return extract_json_with_retry(json_str)
  984. # 方法4: 普通代码块中的 JSON
  985. if "```" in response:
  986. json_start = response.find("```") + 3
  987. json_end = response.find("```", json_start)
  988. json_str = response[json_start:json_end].strip()
  989. if json_str.startswith("json"):
  990. json_str = json_str[4:].strip()
  991. return extract_json_with_retry(json_str)
  992. # 方法5: 尝试提取所有可能的 JSON 对象,优先选择包含 'content' 字段的
  993. # 找到所有 { 的位置
  994. json_candidates = []
  995. i = 0
  996. while i < len(response):
  997. if response[i] == '{':
  998. brace_count = 0
  999. brace_start = i
  1000. brace_end = i
  1001. for j in range(i, len(response)):
  1002. if response[j] == '{':
  1003. brace_count += 1
  1004. elif response[j] == '}':
  1005. brace_count -= 1
  1006. if brace_count == 0:
  1007. brace_end = j + 1
  1008. break
  1009. if brace_end > brace_start:
  1010. json_str = response[brace_start:brace_end]
  1011. try:
  1012. # 尝试解析这个 JSON
  1013. parsed = extract_json_with_retry(json_str)
  1014. # 检查是否包含必需的字段
  1015. if isinstance(parsed, dict):
  1016. json_candidates.append((parsed, json_str))
  1017. except:
  1018. pass
  1019. i = brace_end
  1020. else:
  1021. i += 1
  1022. else:
  1023. i += 1
  1024. # 优先选择包含 'content' 字段的 JSON
  1025. if json_candidates:
  1026. # 首先尝试找到包含 'content' 字段的
  1027. for parsed, json_str in json_candidates:
  1028. if 'content' in parsed and parsed.get('content'):
  1029. print(f"▸ 找到包含 'content' 字段的 JSON(长度: {len(json_str)} 字符)")
  1030. return parsed
  1031. # 如果没有找到包含 'content' 的,选择最完整的 JSON(字段最多的)
  1032. best_candidate = max(json_candidates, key=lambda x: len(x[0]))
  1033. print(f"▸️ 未找到包含 'content' 字段的 JSON,使用最完整的 JSON(字段数: {len(best_candidate[0])})")
  1034. return best_candidate[0]
  1035. # 如果都失败了,抛出错误并显示响应内容
  1036. print(f"▸️ 无法从响应中提取 JSON")
  1037. print(f" 响应完整内容(前2000字符):\n{response[:2000]}")
  1038. raise ValueError("响应中未找到有效的 JSON 数据")
  1039. except Exception as e:
  1040. print(f"▸️ 提取 JSON 时发生错误: {e}")
  1041. print(f" 响应内容(前1000字符): {response[:1000]}")
  1042. raise
  1043. class ReflectionWriterAgent:
  1044. """
  1045. 反思写作 Agent - 使用 ReflectionAgent 模式
  1046. ReflectionAgent 通过自我反思和迭代优化来改进输出,将评审和修改整合为一个 Agent:
  1047. 1. 生成初稿
  1048. 2. 自我评审(反思)
  1049. 3. 根据反思修改(优化)
  1050. 4. 达到质量标准
  1051. """
  1052. def __init__(self):
  1053. self.llm = LLMService.get_llm()
  1054. # 自定义 Reflection 提示词
  1055. reflection_prompts = {
  1056. "initial": """
  1057. 你是一位专业的内容创作者。请撰写以下内容的初稿:
  1058. {task}
  1059. 请输出完整的 JSON 格式内容。
  1060. """,
  1061. "reflect": """
  1062. 你是一位严格的内容评审专家。请评审以下内容:
  1063. # 写作任务: {task}
  1064. # 内容初稿: {content}
  1065. 请从以下维度评审:
  1066. 1. **内容质量** (40分): 准确性、完整性、深度、原创性
  1067. 2. **结构逻辑** (30分): 层次清晰、逻辑连贯、过渡自然
  1068. 3. **语言表达** (20分): 易读性、专业性、准确性
  1069. 4. **格式规范** (10分): 字数达标、格式正确、排版美观
  1070. 如果内容质量很好(85分以上),请回答"无需改进"。
  1071. 否则,请详细指出问题并提供具体的修改建议。
  1072. """,
  1073. "refine": """
  1074. 请根据评审意见优化你的内容:
  1075. # 原始任务: {task}
  1076. # 当前内容: {last_attempt}
  1077. # 评审意见: {feedback}
  1078. 请输出优化后的完整 JSON 格式内容。
  1079. """
  1080. }
  1081. self.agent = ReflectionAgent(
  1082. name="反思写作专家",
  1083. llm=self.llm,
  1084. custom_prompts=reflection_prompts,
  1085. max_iterations=2 # 最多反思 2 次
  1086. )
  1087. def generate_and_refine_content(
  1088. self,
  1089. node: ContentNode,
  1090. context: Dict[str, Any],
  1091. level: int
  1092. ) -> Dict[str, Any]:
  1093. """
  1094. 生成并反思优化内容
  1095. Args:
  1096. node: 当前节点
  1097. context: 写作上下文
  1098. level: 当前层级
  1099. Returns:
  1100. 优化后的内容数据
  1101. """
  1102. print(f"\n▸ ReflectionAgent 开始写作并自我反思...")
  1103. print(f" 使用模式: 初稿 → 自我评审 → 优化")
  1104. structure_requirements = get_structure_requirements(level)
  1105. word_count = get_word_count(level)
  1106. task_description = f"""
  1107. ## 写作任务
  1108. **层级**: Level {level}/3
  1109. **话题**: {node.title}
  1110. **描述**: {node.description}
  1111. **要求字数**: {word_count} 字(允许误差±10%)
  1112. **结构要求**:
  1113. {structure_requirements}
  1114. **上下文**:
  1115. {json.dumps(context, ensure_ascii=False, indent=2)}
  1116. 请输出完整的 JSON 格式内容:
  1117. ```json
  1118. {{
  1119. "title": "章节标题",
  1120. "level": {level},
  1121. "content": "正文内容(markdown格式)",
  1122. "word_count": 实际字数,
  1123. "needs_expansion": true/false,
  1124. "subsections": [...],
  1125. "metadata": {{...}}
  1126. }}
  1127. ```
  1128. """
  1129. response = self.agent.run(task_description)
  1130. content_data = self._extract_json(response)
  1131. print(f"▸ ReflectionAgent 完成反思优化")
  1132. return content_data
  1133. def _extract_json(self, response: str) -> Dict[str, Any]:
  1134. """从响应中提取 JSON"""
  1135. try:
  1136. if response.strip().startswith('{'):
  1137. return json.loads(response)
  1138. if "```json" in response:
  1139. json_start = response.find("```json") + 7
  1140. json_end = response.find("```", json_start)
  1141. json_str = response[json_start:json_end].strip()
  1142. elif "```" in response:
  1143. json_start = response.find("```") + 3
  1144. json_end = response.find("```", json_start)
  1145. json_str = response[json_start:json_end].strip()
  1146. elif "{" in response and "}" in response:
  1147. json_start = response.find("{")
  1148. json_end = response.rfind("}") + 1
  1149. json_str = response[json_start:json_end]
  1150. else:
  1151. raise ValueError("响应中未找到 JSON 数据")
  1152. return json.loads(json_str)
  1153. except Exception as e:
  1154. print(f"▸️ JSON 解析失败: {e}")
  1155. raise