workflow_loop.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import sys
  2. import os
  3. import asyncio
  4. import json
  5. import base64
  6. import re
  7. # 将项目根目录加入,方便导入内部包
  8. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
  9. from agent.llm.qwen import qwen_llm_call
  10. from agent.tools.builtin.search import search_posts
  11. # -----------------
  12. # Tools definitions
  13. # -----------------
  14. async def call_banana_tool(prompt: str) -> str:
  15. """包装 call_banana.py 工具的调用方法,抓取它保存本地的文件路径"""
  16. print(f"\n[Tool] ✨ 正在调用 call_banana 生成图片, Prompt: {prompt[:50]}...")
  17. script_path = os.path.join(os.path.dirname(__file__), "call_banana.py")
  18. # 设置环境变量走兼容模式,同时强制指定 UTF-8 编码避免 Windows 下输出由于表情符号崩溃
  19. env = os.environ.copy()
  20. env["PYTHONIOENCODING"] = "utf-8"
  21. process = await asyncio.create_subprocess_exec(
  22. sys.executable, script_path, "-p", prompt,
  23. stdout=asyncio.subprocess.PIPE,
  24. stderr=asyncio.subprocess.PIPE,
  25. env=env
  26. )
  27. stdout, stderr = await process.communicate()
  28. output = stdout.decode('utf-8', errors='replace')
  29. err_output = stderr.decode('utf-8', errors='replace')
  30. if err_output:
  31. output += "\n" + err_output
  32. # 解析输出:"💾 已保存到本地 -> banana_output_0.jpeg"
  33. match = re.search(r"已保存到本地 -> (.+)", output)
  34. if match:
  35. path = match.group(1).strip()
  36. print(f"[Tool] ✅ call_banana 返回图片路径: {path}")
  37. return path
  38. else:
  39. print(f"[Tool] ❌ call_banana 似乎未成功生成文件, 控制台输出:\n{output}")
  40. return f"Tool Execution Failed. output:\n{output}"
  41. async def search_tool(keyword: str) -> str:
  42. print(f"\n[Tool] 🔍 启动小红书调研, 关键词: {keyword}")
  43. try:
  44. result = await search_posts(keyword=keyword, channel="xhs", max_count=3)
  45. return result.output
  46. except Exception as e:
  47. return f"查询失败: {e}"
  48. def get_agent_tools():
  49. return [
  50. {
  51. "type": "function",
  52. "function": {
  53. "name": "search_tool",
  54. "description": "如果需要了解某个风格如何写 Prompt(例如“写实风格提示词”),调用此工具进行小红书全网搜索,返回总结经验以更新你的参数。",
  55. "parameters": {
  56. "type": "object",
  57. "properties": {
  58. "keyword": {
  59. "type": "string",
  60. "description": "搜索关键词"
  61. }
  62. },
  63. "required": ["keyword"]
  64. }
  65. }
  66. },
  67. {
  68. "type": "function",
  69. "function": {
  70. "name": "call_banana_tool",
  71. "description": "使用此工具通过给定的详细提示词生成图片。工具将返回生成图片的本地保存路径。",
  72. "parameters": {
  73. "type": "object",
  74. "properties": {
  75. "prompt": {
  76. "type": "string",
  77. "description": "英语或中文详细的生图提示词"
  78. }
  79. },
  80. "required": ["prompt"]
  81. }
  82. }
  83. }
  84. ]
  85. # -----------------
  86. # Agent 2: Image Evaluator (Qwen-VL-Max)
  87. # -----------------
  88. async def evaluate_images(target_image_path: str, generated_image_path: str, previous_feedback: str = None) -> str:
  89. print(f"\n[Agent 2] 👁️ Qwen-VL 开始视觉评估...")
  90. print(f" - 目标图: {target_image_path}")
  91. print(f" - 生成图: {generated_image_path}")
  92. def encode_image(image_path):
  93. with open(image_path, "rb") as image_file:
  94. return base64.b64encode(image_file.read()).decode('utf-8')
  95. try:
  96. target_b64 = encode_image(target_image_path)
  97. gen_b64 = encode_image(generated_image_path)
  98. target_ext = target_image_path.split('.')[-1].lower()
  99. if target_ext == 'jpg': target_ext = 'jpeg'
  100. gen_ext = generated_image_path.split('.')[-1].lower()
  101. if gen_ext == 'jpg': gen_ext = 'jpeg'
  102. except Exception as e:
  103. return f"无法读取图片以进行评估: {e}"
  104. system_content = "你是专业的AI生图评审师。你的工作是对比【目标参考图】和当前【生成图】,找出具体的差异,并给出针对性的修改意见给生图Prompt工程师。"
  105. if previous_feedback:
  106. system_content += "\n你还会收到你【上一轮的评估反馈】。请结合你的旧反馈,检查这轮新图片是否修正了你上次提出的问题,避免重复说一样的话,而是要有动态进展意识!"
  107. text_prompt = "请做详细的差异点分析:从构图、色彩、人物或物体细节、整体质感等方面指出当前生成图与目标图的差距。"
  108. if previous_feedback:
  109. text_prompt += f"\n\n你对上一版旧图的评估反馈曾经是:\n{previous_feedback}\n\n请比对这张【新生成图】,告诉我:上一版的问题被解决了吗?画面的进步点和退步点在哪里?请给出更新的针对性修改意见!"
  110. else:
  111. text_prompt += "结束时,请给出具体的 Prompt 修改建议。"
  112. messages = [
  113. {
  114. "role": "system",
  115. "content": system_content
  116. },
  117. {
  118. "role": "user",
  119. "content": [
  120. {"type": "text", "text": "【目标参考图(理想状态)】:"},
  121. {"type": "image_url", "image_url": {"url": f"data:image/{target_ext};base64,{target_b64}"}},
  122. {"type": "text", "text": "【本次生成的图片】:"},
  123. {"type": "image_url", "image_url": {"url": f"data:image/{gen_ext};base64,{gen_b64}"}},
  124. {"type": "text", "text": text_prompt}
  125. ]
  126. }
  127. ]
  128. try:
  129. response = await qwen_llm_call(
  130. messages=messages,
  131. model="qwen3.5-plus"
  132. )
  133. analysis = response["content"]
  134. print(f"\n[Agent 2] 📃 评估反馈:\n{analysis}\n")
  135. return analysis
  136. except Exception as e:
  137. print(f"\n[Agent 2] ⚠️ 评估发生错误: {e}")
  138. return f"VL模型调用失败: {e}"
  139. # -----------------
  140. # Main Workflow Loop
  141. # -----------------
  142. def get_base64_url(image_path: str) -> str:
  143. with open(image_path, "rb") as image_file:
  144. b64_data = base64.b64encode(image_file.read()).decode('utf-8')
  145. ext = image_path.split('.')[-1].lower()
  146. if ext == 'jpg': ext = 'jpeg'
  147. return f"data:image/{ext};base64,{b64_data}"
  148. async def main():
  149. import argparse
  150. import os
  151. default_target = os.path.join(os.path.dirname(os.path.abspath(__file__)), "input", "img_1.png")
  152. parser = argparse.ArgumentParser(description="多智能体画图自动优化 Workflow")
  153. parser.add_argument("-t", "--target", default=default_target, help="你想逼近的目标参考图本地路径")
  154. parser.add_argument("-m", "--max_loops", type=int, default=10, help="优化的最大迭代论调")
  155. args = parser.parse_args()
  156. target_image = args.target
  157. print("\n" + "="*50)
  158. print("🤖 启动双 Agent 生图闭环工作流 (纯 Vision-Language 架构)")
  159. print("="*50)
  160. if not os.path.exists(target_image):
  161. print(f"⚠️ 找不到目标图片: {target_image}")
  162. print("提示: 系统依然会运行寻找文件,但 Agent 2 将无法给出评估。可随便放一个图片来模拟。")
  163. system_msg = {
  164. "role": "system",
  165. "content": "你是一个超级提示词工程师(Prompt Engineer)。目标:生成一张无限接近【目标参考图】的图片。\n作为多模态大模型,每一轮我都会给你看你上次生成的图片结果和评估专家的犀利分析反馈。你需要利用这些反馈进行修改。\n流程要求:\n1. (可选)如果你对风格不确定,可以请求 search_tool 调研别人怎么写相关提示词。\n2. 使用 call_banana_tool 来实际提交你的提示词并生成图片。\n3. 调用生成工具后,你本轮的工作就结束了,系统会把成果拿去评估并在下一轮找你。"
  166. }
  167. max_loops = args.max_loops
  168. current_generation_loop_count = 0
  169. last_gen_info = None
  170. prompt_history = [] # 记录完整的历史 Prompt 轨迹,防止反复抽卡
  171. while current_generation_loop_count < max_loops:
  172. print(f"\n" + "="*40)
  173. print(f"🔄 优化循环: 第 {current_generation_loop_count + 1}/{max_loops} 轮")
  174. print("="*40)
  175. # 每轮重置上下文,只保留 system message 和含有"上次结果"的 initial user message
  176. messages = [system_msg]
  177. if last_gen_info is None:
  178. try:
  179. target_b64_url = get_base64_url(target_image)
  180. messages.append({
  181. "role": "user",
  182. "content": [
  183. {"type": "text", "text": "这是你需要逼近的【目标参考图】。现在请你仔细观察它,并提炼出一份详尽的初步生图 Prompt。你可以酌情使用 search_tool 调研,最后必须使用 call_banana_tool 提交你的 Prompt 生成最初的原型。"},
  184. {"type": "image_url", "image_url": {"url": target_b64_url}}
  185. ]
  186. })
  187. except Exception as e:
  188. messages.append({
  189. "role": "user",
  190. "content": f"目标图片凭据读取失败({e}),请盲猜一个初始 Prompt 并使用 call_banana_tool 生成。"
  191. })
  192. else:
  193. try:
  194. gen_image_url = get_base64_url(last_gen_info["image_path"])
  195. # 构建历史记录描述,让它知道自己之前走过哪些弯路避免抽卡
  196. history_text = "【你的历史迭代轨迹 (包含往期Prompt与评估专家对其的批评,用于防复读和总结改进)】:\n"
  197. for i, record in enumerate(prompt_history):
  198. history_text += f"==== 第 {i+1} 轮 ====\n"
  199. history_text += f"[使用的 Prompt]:\n{record['prompt']}\n"
  200. history_text += f"[收到的反馈批评]:\n{record['feedback']}\n\n"
  201. messages.append({
  202. "role": "user",
  203. "content": [
  204. {"type": "text", "text": f"{history_text}\n这可以帮你回顾你之前走过的路径。现在聚焦到上一轮:\n\n你上一轮({len(prompt_history)})使用的生图Prompt为:\n{last_gen_info['prompt']}\n\n这里是你上一轮生成的图片结果,请仔细查看对比:"},
  205. {"type": "image_url", "image_url": {"url": gen_image_url}},
  206. {"type": "text", "text": f"【视觉评估专家的分析反馈】:\n{last_gen_info['feedback']}\n\n请针对上述反馈,思考到底哪里不像,参考上述的历史轨迹避免重蹈覆辙,进行新的调研修正(如果需要),或者直接使用 call_banana_tool 生成优化后的版本。"}
  207. ]
  208. })
  209. except Exception as e:
  210. messages.append({
  211. "role": "user",
  212. "content": f"上一轮信息读取失败 ({e})。请重新尝试凭感觉用 call_banana_tool 再次生成。"
  213. })
  214. # Agent 1 内部工具调研微循环 (Agent 1 minor logic loop)
  215. agent1_finished_generation = False
  216. consecutive_empty = 0
  217. while not agent1_finished_generation:
  218. print(f"---\n💬 正在请求 Agent 1 (Prompt 师)...")
  219. # 这里 Agent 1 也换成 qwen-vl-max,这样它才能看到传给它的上一轮图片
  220. response = await qwen_llm_call(
  221. messages=messages,
  222. model="qwen3.5-plus",
  223. tools=get_agent_tools()
  224. )
  225. content = response.get("content", "")
  226. tool_calls = response.get("tool_calls")
  227. if content:
  228. print(f"\n[Agent 1 思考]:\n{content}")
  229. if not tool_calls and not content:
  230. consecutive_empty += 1
  231. if consecutive_empty >= 3:
  232. print("Agent 连续多次无有意义输出,强制跳出本轮。")
  233. break
  234. else:
  235. consecutive_empty = 0
  236. # 保持上下文
  237. assistant_reply = {"role": "assistant"}
  238. if content: assistant_reply["content"] = content
  239. if tool_calls: assistant_reply["tool_calls"] = tool_calls
  240. messages.append(assistant_reply)
  241. if tool_calls:
  242. for tc in tool_calls:
  243. func_name = tc["function"]["name"]
  244. args_dict = json.loads(tc["function"]["arguments"])
  245. tc_id = tc["id"]
  246. if func_name == "search_tool":
  247. res = await search_tool(**args_dict)
  248. messages.append({
  249. "role": "tool",
  250. "tool_call_id": tc_id,
  251. "content": str(res)
  252. })
  253. elif func_name == "call_banana_tool":
  254. print(f"\n⚙️ Agent 1 决定提交生图请求!")
  255. gen_path = await call_banana_tool(**args_dict)
  256. # ⚠️ 把生成的图片按轮次重命名防覆盖,保存中间过程
  257. if os.path.exists(gen_path):
  258. ext = gen_path.split('.')[-1]
  259. new_gen_path = f"gen_loop_{current_generation_loop_count + 1}.{ext}"
  260. import shutil
  261. shutil.move(gen_path, new_gen_path)
  262. gen_path = new_gen_path
  263. print(f"[文件管理] 中间图片已重命名并保存为: {new_gen_path}")
  264. prompt_used = args_dict.get("prompt", "")
  265. # 把消息补齐,虽然这一轮马上就要重置销毁了
  266. messages.append({
  267. "role": "tool",
  268. "tool_call_id": tc_id,
  269. "content": f"已生成,路径: {gen_path}"
  270. })
  271. agent1_finished_generation = True
  272. current_generation_loop_count += 1
  273. # 进行评估并记录,传递给下一大轮
  274. if os.path.exists(gen_path) and os.path.exists(target_image):
  275. prev_feedback = last_gen_info["feedback"] if last_gen_info else None
  276. evaluation_feedback = await evaluate_images(target_image, gen_path, prev_feedback)
  277. last_gen_info = {
  278. "prompt": prompt_used,
  279. "image_path": gen_path,
  280. "feedback": evaluation_feedback
  281. }
  282. else:
  283. last_gen_info = {
  284. "prompt": prompt_used,
  285. "image_path": gen_path,
  286. "feedback": f"系统提示:由于目标图 {target_image} 或生成图 {gen_path} 不存在,评估被跳过。"
  287. }
  288. # 记录到全局大历史中,供它长线参考防重踩坑
  289. prompt_history.append(last_gen_info)
  290. break # 跳出 tool_calls for loop
  291. else:
  292. # 没调工具
  293. print("\n[控制中心] Agent 1 没有继续使用任何工具。结束其周期。")
  294. agent1_finished_generation = True
  295. break
  296. print("\n🎉 工作流闭环成功完成或达到了最大迭代次数。")
  297. # 最后由评估专家出具一份最完善的多维度最终报告
  298. if len(prompt_history) > 0 and os.path.exists(target_image):
  299. print("\n" + "="*50)
  300. print("🏆 正在生成【专家最终多维度反馈报告】...")
  301. print("="*50)
  302. first_gen = prompt_history[0]["image_path"]
  303. last_gen = prompt_history[-1]["image_path"]
  304. if os.path.exists(first_gen) and os.path.exists(last_gen):
  305. try:
  306. target_b64 = encode_image(target_image)
  307. first_b64 = encode_image(first_gen)
  308. last_b64 = encode_image(last_gen)
  309. target_ext = target_image.split('.')[-1].lower()
  310. first_ext = first_gen.split('.')[-1].lower()
  311. last_ext = last_gen.split('.')[-1].lower()
  312. # 构建供最终分析的文字轨迹
  313. full_history_text = "【历次 Prompt 与专家反馈的演进轨迹】\n"
  314. for i, record in enumerate(prompt_history):
  315. full_history_text += f"-- 第 {i+1} 轮 --\n[Prompt]: {record['prompt']}\n[反馈]: {record['feedback']}\n\n"
  316. final_messages = [
  317. {
  318. "role": "system",
  319. "content": "你是首席AI打样架构师。目前的生图迭代优化工作流已拉下帷幕。你不需要拘泥于打分,而是要通过回顾整个演进历程,总结出‘最好用的 Prompt 模板’和‘最精准的评估反馈维度模板’。"
  320. },
  321. {
  322. "role": "user",
  323. "content": [
  324. {"type": "text", "text": "【目标参考图(原图)】:"},
  325. {"type": "image_url", "image_url": {"url": f"data:image/{target_ext if target_ext != 'jpg' else 'jpeg'};base64,{target_b64}"}},
  326. {"type": "text", "text": "这是最初第1轮盲试的生成图:"},
  327. {"type": "image_url", "image_url": {"url": f"data:image/{first_ext if first_ext != 'jpg' else 'jpeg'};base64,{first_b64}"}},
  328. {"type": "text", "text": f"这是经过迭代后的【最终生成图】:"},
  329. {"type": "image_url", "image_url": {"url": f"data:image/{last_ext if last_ext != 'jpg' else 'jpeg'};base64,{last_b64}"}},
  330. {"type": "text", "text": f"下面是 {len(prompt_history)} 轮迭代中,Prompt 和专家反馈的完整变迁记录:\n\n{full_history_text}\n\n请结合首尾图片的巨大差异以及中间的踩坑过程,深度复盘:\n1. 在构建生图 Prompt 时,哪些描述方式、句型或结构最能有效命中模型?请提炼出一个【最终版高转化率 Prompt 语法模板】。\n2. 在进行视觉反馈时,哪些维度的批评和建议对 Prompt 师是最具指导意义的?请提炼出一个【最终版高维度视觉评估反馈模板】。\n这两个模版需要具备极强的通用性和实战复用价值!"}
  331. ]
  332. }
  333. ]
  334. response = await qwen_llm_call(
  335. messages=final_messages,
  336. model="qwen3.5-plus"
  337. )
  338. print(f"\n[Agent 2] 📋 【最终多维度评估报告】:\n{response['content']}\n")
  339. except Exception as e:
  340. print(f"最终报告生成失败: {e}")
  341. if __name__ == "__main__":
  342. asyncio.run(main())