kit初版,模型引入,agent优化

This commit is contained in:
2026-04-13 00:41:23 +08:00
parent 9b053e302b
commit c435ab15cf
14097 changed files with 5032 additions and 2676248 deletions

View File

@@ -1,15 +1,19 @@
"""Agent Loop 主循环LLM 对话 -> 工具调用 -> 结果回传 -> 继续。"""
import json
import os
from typing import AsyncGenerator, Optional
from openai import AsyncOpenAI
from app.agent.tools import TOOL_DEFINITIONS, execute_tool
from app.config import get_llm_model, get_llm_max_iterations
from app.services.image_gen import to_data_uri
def _get_client() -> AsyncOpenAI:
return AsyncOpenAI()
base_url = os.getenv("OPENAI_BASE_URL")
return AsyncOpenAI(base_url=base_url) if base_url else AsyncOpenAI()
SYSTEM_PROMPT = """\
你是一个专业的游戏美术 AI 助手。你的工作是帮助美术人员通过对话生成游戏美术资源。
@@ -19,6 +23,7 @@ SYSTEM_PROMPT = """\
- 理解用户的审美意图,将中文描述转化为高质量的英文生成 prompt
- 根据用户反馈迭代修改(调整颜色、风格、构图等)
- 如果用户提供了参考图,将参考图的风格元素融入生成 prompt
- 理解用户在图片上的标注(框选区域 + 文字批注),精准定位需要修改的部分
## 工作流程
1. 理解用户需求,必要时追问细节(尺寸、风格、用途等)
@@ -26,11 +31,20 @@ SYSTEM_PROMPT = """\
3. 向用户展示结果并询问反馈
4. 根据反馈调整 prompt 并重新生成
## 标注理解
当用户发送带有「图片标注」的消息时,表示用户在之前生成的图片上做了标注。
标注格式为:`[区域 (x%, y%) 大小 w%×h%]: 修改意见`
- 区域坐标表示标注框在图片上的相对位置
- 你需要理解标注区域所指的图片内容,并将修改意见融入新的 prompt
- 如果附带了标注截图(参考图),仔细观察红色标注框和文字来理解用户意图
- 在调整 prompt 时,保持原图整体风格不变,只针对标注区域做修改
## 生成 prompt 要求
- 必须使用英文
- 尽量详细描述:主体内容、风格、颜色方案、光照、构图、材质等
- 尽量详细描述:主体内容、颜色方案、光照、构图、材质等
- 如果用户要求游戏 UI 元素,添加相关关键词如 "game UI", "icon", "button"
- 如果用户提供了参考图,在 prompt 中描述参考图的风格特征
- 如果你能直接看到参考图(图片内容),可以在 prompt 中描述参考图的风格特征
- 如果你无法看到参考图(只收到了文字提示说有参考图),参考图会由生图工具的 IP-Adapter 自动处理风格融合。此时你的 prompt 中**绝对不要猜测或指定任何画风/艺术风格关键词**(如 pixel art、watercolor、oil painting 等),只描述画面内容(主体、构图、光照等),把风格完全交给参考图来决定
## 注意事项
- 用中文和用户交流
@@ -42,6 +56,7 @@ SYSTEM_PROMPT = """\
async def run_agent_loop(
messages: list[dict],
ref_image_url: Optional[str] = None,
image_model: Optional[str] = None,
) -> AsyncGenerator[dict, None]:
"""
运行 Agent Loop以 SSE 事件流形式 yield 结果。
@@ -56,33 +71,45 @@ async def run_agent_loop(
# 构建消息列表
api_messages = [{"role": "system", "content": SYSTEM_PROMPT}]
# 检测当前 LLM 是否支持 vision多模态图片输入
llm_model = get_llm_model().lower()
vision_capable = any(kw in llm_model for kw in ("gpt-4o", "gpt-4-vision", "claude"))
for msg in messages:
if msg["role"] == "user" and ref_image_url:
# 最后一条用户消息附加参考图GPT-4o vision
if msg == messages[-1] or (
msg.get("role") == "user"
and messages.index(msg) == len(messages) - 1
):
if msg["role"] == "user" and ref_image_url and msg is messages[-1]:
if vision_capable:
# 支持 vision 的模型:直接发送图片
api_messages.append({
"role": "user",
"content": [
{"type": "text", "text": msg["content"]},
{
"type": "image_url",
"image_url": {"url": ref_image_url},
"image_url": {"url": to_data_uri(ref_image_url)},
},
],
})
continue
else:
# 不支持 vision 的模型:以文字提示告知有参考图,风格由 IP-Adapter 处理
hint = (
f"{msg['content']}\n\n"
"【系统提示:用户上传了一张参考图,已自动传递给图片生成工具的 IP-Adapter。"
"IP-Adapter 会从参考图中提取风格并融合到生成结果中。"
"你无法看到这张参考图,因此在生成 prompt 时:\n"
"1. 只描述画面内容(主体、构图、光照、材质等)\n"
"2. 不要猜测或添加任何画风/艺术风格关键词(如 pixel art、watercolor、cartoon 等)\n"
"3. 风格完全由参考图通过 IP-Adapter 决定】"
)
api_messages.append({"role": "user", "content": hint})
continue
api_messages.append({"role": msg["role"], "content": msg["content"]})
client = _get_client()
max_iterations = 5
for _ in range(max_iterations):
for _ in range(get_llm_max_iterations()):
try:
response = await client.chat.completions.create(
model="gpt-4o-mini",
model=get_llm_model(),
messages=api_messages,
tools=TOOL_DEFINITIONS,
stream=True,
@@ -163,13 +190,28 @@ async def run_agent_loop(
except json.JSONDecodeError:
arguments = {}
result = await execute_tool(tool_name, arguments, ref_image_url)
result = await execute_tool(tool_name, arguments, ref_image_url, image_model)
used_model = result.get("model_name", "")
if result.get("errors"):
yield {
"type": "tool_error",
"data": {
"tool": tool_name,
"errors": result["errors"],
"model_name": used_model,
},
}
# 如果有图片结果,推送给前端
if result.get("images"):
yield {
"type": "image_result",
"data": {"images": result["images"]},
"data": {
"images": result["images"],
"prompt_used": result.get("prompt_used", ""),
"model_name": used_model,
},
}
# 工具结果回传给 LLM
@@ -179,7 +221,7 @@ async def run_agent_loop(
"content": json.dumps(result, ensure_ascii=False),
})
# 清除 ref_image_url,避免后续轮次重复附加
ref_image_url = None
# ref_image_url 不清除:消息构建只在循环外执行一次,
# 后续工具调用仍需参考图InstantStyle 等模型必须有 style_image
yield {"type": "done", "data": {}}

View File

@@ -38,20 +38,27 @@ async def execute_tool(
tool_name: str,
arguments: dict,
ref_image_url: str | None = None,
image_model: str | None = None,
) -> dict:
"""执行工具调用,返回结果。"""
if tool_name == "generate_image":
prompt = arguments["prompt"]
num_images = arguments.get("num_images", 1)
image_urls = await generate_images(
result = await generate_images(
prompt=prompt,
num_images=num_images,
ref_image_url=ref_image_url,
model_id=image_model,
)
valid_urls = [u for u in result.urls if not u.startswith("[")]
errors = [u for u in result.urls if u.startswith("[")]
return {
"success": True,
"images": image_urls,
"success": len(valid_urls) > 0,
"images": valid_urls,
"errors": errors,
"prompt_used": prompt,
"model_name": result.model_name,
"model_id": result.model_id,
}
return {"success": False, "error": f"未知工具: {tool_name}"}