加了一堆模型和一堆功能

This commit is contained in:
2026-04-16 00:36:21 +08:00
parent 47c0863bab
commit 40de992516
34 changed files with 3111 additions and 718 deletions

View File

@@ -11,7 +11,7 @@ from openai import AsyncOpenAI
from app.agent.tools import TOOL_DEFINITIONS, execute_tool
from app.config import (
get_llm_model,
get_llm_model_config,
get_llm_max_iterations,
get_image_model_config,
get_max_recent_turns,
@@ -22,9 +22,19 @@ from app.services.image_gen import to_data_uri
logger = logging.getLogger(__name__)
def _get_client() -> AsyncOpenAI:
base_url = os.getenv("OPENAI_BASE_URL")
return AsyncOpenAI(base_url=base_url) if base_url else AsyncOpenAI()
def _get_client(provider: str) -> AsyncOpenAI:
"""按 provider 创建对应的 OpenAI 兼容客户端。"""
if provider == "vectorengine":
return AsyncOpenAI(
api_key=os.getenv("VECTORENGINE_API_KEY"),
base_url=os.getenv("VECTORENGINE_BASE_URL", "https://api.vectorengine.ai/v1"),
)
if provider == "deepseek":
return AsyncOpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
)
return AsyncOpenAI()
SYSTEM_PROMPT = """\
你是一个专业的游戏美术 AI 助手。你的工作是帮助美术人员通过对话生成游戏美术资源。
@@ -33,7 +43,8 @@ SYSTEM_PROMPT = """\
- 根据用户的文字描述生成图片UI图标、按钮、插画、立绘、概念图等
- 理解用户的审美意图,将中文描述转化为高质量的英文生成 prompt
- 根据用户反馈迭代修改(调整颜色、风格、构图等)
- 如果用户提供了参考图,将参考图的风格元素融入生成 prompt
- 如果用户提供了参考图(支持多张),将参考图的风格元素融入生成 prompt
- 理解多张参考图各自的角色(如"图1的主体 + 图2的风格/视角"),并在 prompt 中准确传达
- 理解用户在图片上的标注(框选区域 + 文字批注),精准定位需要修改的部分
## 工作流程
@@ -54,23 +65,25 @@ SYSTEM_PROMPT = """\
- 必须使用英文
- 尽量详细描述:主体内容、颜色方案、光照、构图、材质等
- 如果用户要求游戏 UI 元素,添加相关关键词如 "game UI", "icon", "button"
- 如果你能直接看到参考图(图片内容),可以在 prompt 中描述参考图的风格特征
- 如果你无法看到参考图(只收到了文字提示说有参考图),参考图会由生图工具的 IP-Adapter 自动处理风格融合。此时你不要自行猜测画风/艺术风格关键词(如 pixel art、watercolor、oil painting 等),把风格交给参考图来决定。但如果用户在消息中明确指定了风格(如"赛博朋克风""水彩风"等),应保留并翻译到 prompt 中——尊重用户的主动意图
- 如果你能直接看到参考图(图片内容),可以在 prompt 中描述参考图的风格特征。多张参考图时,理解用户对各图的定位(如"图1做主体参考、图2做风格参考"),将相应特征分别融入 prompt
- 如果你无法看到参考图(只收到了文字提示说有参考图),参考图会由生图工具自动处理风格融合。此时你不要自行猜测画风/艺术风格关键词(如 pixel art、watercolor、oil painting 等),把风格交给参考图来决定。但如果用户在消息中明确指定了风格(如"赛博朋克风""水彩风"等),应保留并翻译到 prompt 中——尊重用户的主动意图
## 注意事项
- 用中文和用户交流
- 生成图片后简要说明你使用的 prompt 思路
- 主动建议迭代方向
- **禁止模拟工具调用**:生成图片时必须实际调用 generate_image 工具,绝不能用文字描述"已生成"或假装工具已执行。如果需要生成多张图片,每张都必须单独调用工具
- **禁止在回复中嵌入图片链接**:不要在回复文字中使用 Markdown 图片语法(如 `![](...)` 或 `sandbox:` 链接)。图片展示由系统自动处理,你只需用文字描述结果即可
"""
async def run_agent_loop(
messages: list[dict],
ref_image_url: Optional[str] = None,
ref_image_urls: Optional[list[str]] = None,
image_model: Optional[str] = None,
session_id: Optional[str] = None,
user_id: str = "default_user",
llm_model: Optional[str] = None,
) -> AsyncGenerator[dict, None]:
"""
运行 Agent Loop以 SSE 事件流形式 yield 结果。
@@ -114,10 +127,16 @@ async def run_agent_loop(
yield {"type": "error", "data": {"message": f"记忆系统检索失败: {e}"}}
return
# ── 解析 LLM 模型配置 ──
llm_config = get_llm_model_config(llm_model)
llm_provider = llm_config["provider"]
llm_model_id = llm_config["model_id"]
vision_capable = llm_config.get("vision", False)
# ── 构建 system prompt ──
model_config = get_image_model_config(image_model)
current_model_name = model_config.get('name', '未知')
current_model_id = model_config.get('id', '未知')
img_model_config = get_image_model_config(image_model)
current_model_name = img_model_config.get('name', '未知')
current_model_id = img_model_config.get('id', '未知')
model_hint = (
f"\n\n## 当前生图模型(重要)\n"
f"本次对话用户选择的生图模型是 **{current_model_name}**"
@@ -127,43 +146,39 @@ async def run_agent_loop(
)
api_messages = [{"role": "system", "content": SYSTEM_PROMPT + model_hint + memory_block}]
# 检测当前 LLM 是否支持 vision多模态图片输入
llm_model = get_llm_model().lower()
vision_capable = any(kw in llm_model for kw in ("gpt-4o", "gpt-4-vision", "claude"))
effective_refs = ref_image_urls or []
for msg in messages:
if msg["role"] == "user" and ref_image_url and msg is messages[-1]:
if msg["role"] == "user" and effective_refs and msg is messages[-1]:
if vision_capable:
api_messages.append({
"role": "user",
"content": [
{"type": "text", "text": msg["content"]},
{
"type": "image_url",
"image_url": {"url": to_data_uri(ref_image_url)},
},
],
})
content_parts: list[dict] = [{"type": "text", "text": msg["content"]}]
for ref_url in effective_refs:
content_parts.append({
"type": "image_url",
"image_url": {"url": to_data_uri(ref_url)},
})
api_messages.append({"role": "user", "content": content_parts})
else:
n_refs = len(effective_refs)
hint = (
f"{msg['content']}\n\n"
"【系统提示:用户上传了张参考图,已自动传递给图片生成工具的 IP-Adapter"
"IP-Adapter 会从参考图中提取风格融合到生成结果中"
"你无法看到这参考图,因此在生成 prompt 时:\n"
f"【系统提示:用户上传了 {n_refs} 张参考图,已自动传递给图片生成工具。"
"生图工具会根据模型能力自动处理参考图的风格融合。"
"你无法看到这参考图,因此在生成 prompt 时:\n"
"1. 描述画面内容(主体、构图、光照、材质等)\n"
"2. 不要自行猜测画风/艺术风格——但如果用户明确指定了风格,保留到 prompt 中\n"
"3. 用户未指定风格时,风格完全由参考图通过 IP-Adapter 决定】"
"3. 用户未指定风格时,风格完全由参考图决定】"
)
api_messages.append({"role": "user", "content": hint})
continue
api_messages.append({"role": msg["role"], "content": msg["content"]})
client = _get_client()
client = _get_client(llm_provider)
for _ in range(get_llm_max_iterations()):
try:
response = await client.chat.completions.create(
model=get_llm_model(),
model=llm_model_id,
messages=api_messages,
tools=TOOL_DEFINITIONS,
stream=True,
@@ -219,7 +234,7 @@ async def run_agent_loop(
"你刚才没有调用 generate_image 工具,只是用文字描述了生成过程。"
"请立即调用 generate_image 工具来实际生成图片。"
"不要解释,直接调用工具。"
f"当前使用的生图模型是 {model_config.get('name', '未知')}"
f"当前使用的生图模型是 {img_model_config.get('name', '未知')}"
),
})
continue
@@ -263,7 +278,7 @@ async def run_agent_loop(
except json.JSONDecodeError:
arguments = {}
result = await execute_tool(tool_name, arguments, ref_image_url, image_model)
result = await execute_tool(tool_name, arguments, effective_refs or None, image_model)
used_model = result.get("model_name", "")

View File

@@ -11,6 +11,8 @@ TOOL_DEFINITIONS = [
"description": (
"根据文字描述生成图片。prompt 必须是英文。"
"如果用户提供了参考图,会自动传入 ref_image_url 参数。"
"若当前生图模型为 Stable Diffusion XL可在正提示后单独一行写 ---NEGATIVE--- 再写负向提示;"
"不传则服务端会使用该模型的默认负向词。"
),
"parameters": {
"type": "object",
@@ -37,7 +39,7 @@ TOOL_DEFINITIONS = [
async def execute_tool(
tool_name: str,
arguments: dict,
ref_image_url: str | None = None,
ref_image_urls: list[str] | None = None,
image_model: str | None = None,
) -> dict:
"""执行工具调用,返回结果。"""
@@ -47,7 +49,7 @@ async def execute_tool(
result = await generate_images(
prompt=prompt,
num_images=num_images,
ref_image_url=ref_image_url,
ref_image_urls=ref_image_urls,
model_id=image_model,
)
valid_urls = [u for u in result.urls if not u.startswith("[")]
@@ -57,6 +59,8 @@ async def execute_tool(
"images": valid_urls,
"errors": errors,
"prompt_used": prompt,
"effective_prompt": result.effective_prompt,
"negative_prompt": result.negative_prompt,
"model_name": result.model_name,
"model_id": result.model_id,
}