完善模型测试与参考文件历史能力

This commit is contained in:
zwt13703
2026-07-02 17:37:35 +08:00
parent 8743a02110
commit d3530ab0b7
16 changed files with 697 additions and 32 deletions
+142 -4
View File
@@ -43,12 +43,23 @@ def _ensure_json_content(text: str) -> dict:
return {"content": [{"type": "text", "text": stripped}]}
def _format_file_context(file_summaries: list[dict]) -> str:
file_blocks: list[str] = []
for item in file_summaries:
file_name = item.get("file_name") or "未命名文件"
summary = item.get("summary") or "文件内容为空。"
file_blocks.append(f"文件:{file_name}\n内容:\n{summary}")
return "\n\n".join(file_blocks)
def _build_prompt(paragraph: Paragraph, file_summaries: list[dict] | None = None) -> tuple[str, str]:
system_prompt = (
"你是一个企业文档撰写助手。"
"请严格输出 JSON,不要输出 JSON 之外的说明。"
'格式为:{"content":[{"type":"text","text":"..."},{"type":"table","title":"...","headers":["..."],"rows":[["..."]]}]}。'
)
if getattr(paragraph, "enable_reasoning", False):
system_prompt += "你可以先进行充分思考,再给出最终答案,但最终只输出要求的结果内容。"
user_parts = [f"段落标题:{paragraph.title}"]
if paragraph.content:
@@ -56,10 +67,7 @@ def _build_prompt(paragraph: Paragraph, file_summaries: list[dict] | None = None
if paragraph.need_prompt and paragraph.prompt_text:
user_parts.append(f"附加要求:{paragraph.prompt_text}")
if file_summaries:
file_blocks = []
for item in file_summaries:
file_blocks.append(f"文件名:{item['file_name']}\n文件摘要:{item['summary']}")
user_parts.append("参考文件内容:\n" + "\n\n".join(file_blocks))
user_parts.append("参考文件内容:\n" + _format_file_context(file_summaries))
user_parts.append(f"输出格式:{paragraph.output_format}")
return system_prompt, "\n\n".join(user_parts)
@@ -136,6 +144,65 @@ async def _call_openai_compatible(model: AiModel, system_prompt: str, user_promp
return AiCallResult(content=_ensure_json_content(text), raw_text=text, used_model=model.name)
async def _stream_openai_compatible(model: AiModel, system_prompt: str, user_prompt: str):
api_key = decrypt_text(model.api_key_encrypted)
if not api_key:
raise RuntimeError("模型 API Key 不可用")
payload = {
"model": model.name,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"temperature": 0.3,
"stream": True,
}
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
last_error: Exception | None = None
for attempt in range(settings.AI_MAX_RETRIES):
try:
async with httpx.AsyncClient(timeout=settings.AI_REQUEST_TIMEOUT, trust_env=False) as client:
async with client.stream("POST", _normalize_openai_endpoint(model.api_endpoint), headers=headers, json=payload) as response:
if response.status_code in (429, 500, 502, 503, 504):
body = await response.aread()
raise httpx.HTTPStatusError(
f"上游模型流式响应异常: {response.status_code} - {body.decode('utf-8', errors='ignore')[:500]}",
request=response.request,
response=response,
)
response.raise_for_status()
async for line in response.aiter_lines():
if not line or not line.startswith("data:"):
continue
payload_line = line[5:].strip()
if payload_line == "[DONE]":
break
try:
chunk = json.loads(payload_line)
except json.JSONDecodeError:
continue
delta_payload = chunk.get("choices", [{}])[0].get("delta", {})
delta = delta_payload.get("content", "")
reasoning = delta_payload.get("reasoning_content", "")
if isinstance(delta, list):
delta = "".join(
item.get("text", "") if isinstance(item, dict) else str(item)
for item in delta
)
if delta:
yield delta
if reasoning:
yield reasoning
return
except (httpx.TimeoutException, httpx.HTTPStatusError, httpx.NetworkError) as error:
last_error = error
if attempt == settings.AI_MAX_RETRIES - 1:
break
await asyncio.sleep(2 ** attempt)
raise RuntimeError(f"模型流式调用失败:{last_error}")
async def _call_anthropic(model: AiModel, system_prompt: str, user_prompt: str) -> AiCallResult:
api_key = decrypt_text(model.api_key_encrypted)
if not api_key:
@@ -162,8 +229,79 @@ async def _call_anthropic(model: AiModel, system_prompt: str, user_prompt: str)
return AiCallResult(content=_ensure_json_content(text), raw_text=text, used_model=model.name)
async def _stream_anthropic(model: AiModel, system_prompt: str, user_prompt: str):
api_key = decrypt_text(model.api_key_encrypted)
if not api_key:
raise RuntimeError("模型 API Key 不可用")
payload = {
"model": model.name,
"max_tokens": 2048,
"system": system_prompt,
"messages": [{"role": "user", "content": user_prompt}],
"stream": True,
}
headers = {
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
}
last_error: Exception | None = None
for attempt in range(settings.AI_MAX_RETRIES):
try:
async with httpx.AsyncClient(timeout=settings.AI_REQUEST_TIMEOUT, trust_env=False) as client:
async with client.stream("POST", _normalize_anthropic_endpoint(model.api_endpoint), headers=headers, json=payload) as response:
if response.status_code in (429, 500, 502, 503, 504):
body = await response.aread()
raise httpx.HTTPStatusError(
f"上游模型流式响应异常: {response.status_code} - {body.decode('utf-8', errors='ignore')[:500]}",
request=response.request,
response=response,
)
response.raise_for_status()
async for line in response.aiter_lines():
if not line or not line.startswith("data:"):
continue
payload_line = line[5:].strip()
if payload_line == "[DONE]":
break
try:
chunk = json.loads(payload_line)
except json.JSONDecodeError:
continue
if chunk.get("type") == "content_block_delta":
delta = chunk.get("delta", {}).get("text", "")
if delta:
yield delta
return
except (httpx.TimeoutException, httpx.HTTPStatusError, httpx.NetworkError) as error:
last_error = error
if attempt == settings.AI_MAX_RETRIES - 1:
break
await asyncio.sleep(2 ** attempt)
raise RuntimeError(f"模型流式调用失败:{last_error}")
async def call_ai(paragraph: Paragraph, model: AiModel, file_summaries: list[dict] | None = None) -> AiCallResult:
system_prompt, user_prompt = _build_prompt(paragraph, file_summaries)
if model.api_format == "anthropic":
return await _call_anthropic(model, system_prompt, user_prompt)
return await _call_openai_compatible(model, system_prompt, user_prompt)
def build_test_stream_prompt(paragraph: Paragraph, file_summaries: list[dict] | None = None) -> tuple[str, str]:
system_prompt = "你是一个企业文档撰写助手。请直接输出适合预览的正文内容或 Markdown 表格,不要输出 JSON。"
if getattr(paragraph, "enable_reasoning", False):
system_prompt += "你可以先进行充分思考,再持续输出最终可展示的内容。"
_, user_prompt = _build_prompt(paragraph, file_summaries or [])
return system_prompt, user_prompt
async def stream_ai_preview(paragraph: Paragraph, model: AiModel, file_summaries: list[dict] | None = None):
system_prompt, user_prompt = build_test_stream_prompt(paragraph, file_summaries)
if model.api_format == "anthropic":
async for chunk in _stream_anthropic(model, system_prompt, user_prompt):
yield chunk
return
async for chunk in _stream_openai_compatible(model, system_prompt, user_prompt):
yield chunk