"""Agnes 翻译后端(通用 LLM 做翻译,作为 TMT/MaaS 之后的第三级 fallback)。 - 端点:settings.agnes_base_url/v1/chat/completions - 模型:settings.agnes_chat_model(agnes-2.0-flash) - 鉴权:Bearer AGNES_API_KEY - 复用 LlmClient 的限速(interval_sec) 适用场景: - TMT 月配额快满(>95%)时,先走 MaaS(u2) - MaaS 不可用 / 失败时,降到 Agnes(通用 LLM 翻译,质量次之,但够用) - Agnes 不可用时,本地 NLLB 最后兜底 注意: - Agnes 不是翻译专用模型,翻译质量不如 TMT / MaaS u2 - 但 Agnes 的 commentary / image / classification 已经走它,客户端有相同 quota 计费体系 - 用一个特殊 system prompt 强约束:只输出译文 """ from __future__ import annotations import logging from app.config import settings from app.services.llm.client import LlmClient from app.services.translation.base import BaseTranslator, TranslationResult logger = logging.getLogger("news.translate.agnes") # Agnes 做翻译的 system prompt。 # 关键约束:只输出译文,不要思考/分析/注释/标签。 # 这是通用 LLM 翻车最多的地方——会输出 "以下是中文翻译: ..." 前缀,要强约束。 _SYSTEM_PROMPT = """你是即时翻译引擎。把用户输入的文本翻译成简体中文。 严格规则: 1. 只输出译文本身,不要输出任何其他文字 2. 不要输出"以下是翻译"、"译文:"、"[Translation]"、引号、破折号等任何包裹 3. 不要分步思考、不要分析、不要注释、不要解释 4. 不要输出 等内部标签 5. 如果输入已经是中文,直接原样返回 6. 保持原文段落结构(用换行分隔)""" class AgnesTranslator(BaseTranslator): """Agnes 翻译(通用 LLM 兜底)。""" name = "agnes" def __init__(self): if not settings.agnes_api_key: raise RuntimeError("AGNES_API_KEY 未配置") # 用 chat_model,不复用 image_model self.client = LlmClient( base_url=settings.agnes_base_url, api_key=settings.agnes_api_key, chat_model=settings.agnes_chat_model, image_model=settings.agnes_image_model, interval_sec=settings.llm_interval_sec, ) def is_configured(self) -> bool: return bool(settings.agnes_api_key) async def translate( self, text: str, source: str = "auto", target: str = "zh" ) -> TranslationResult: if not text.strip(): return TranslationResult(text=text, engine=self.name, chars=0) if not self.is_configured(): raise RuntimeError("AGNES_API_KEY 未配置") # 长文本截断保护:Agnes max_tokens 上限小,避免超限 # 经验值:1 token ≈ 1.5 个英文 / 0.7 个中文字符 # 默认 chat 接口 max_tokens=1500,我们给 4096 留余量 # 输入截到 6000 字符(MaaS 的限制类似) max_input = 6000 truncated = text[:max_input] if len(text) > max_input else text # 保留段落结构 user = truncated # LlmClient.chat 已含 interval_sec 节流 content = await self.client.chat( system=_SYSTEM_PROMPT, user=user, temperature=0.1, # 翻译任务低温度更稳 max_tokens=4096, ) if not content or not content.strip(): raise RuntimeError("Agnes 返回空 content") # 清洗:模型有时还会输出 markdown 代码块包裹 cleaned = content.strip() if cleaned.startswith("```"): # 去掉代码块围栏 lines = cleaned.split("\n") cleaned = "\n".join( l for l in lines if not l.strip().startswith("```") ).strip() # 去除已知的前缀模式(模型偶尔还是会犯) prefixes_to_strip = [ "以下是译文:", "以下是中文翻译:", "译文:", "中文翻译:", "Translation:", "翻译结果:", ] for p in prefixes_to_strip: if cleaned.startswith(p): cleaned = cleaned[len(p):].lstrip() break return TranslationResult( text=cleaned, engine=self.name, chars=len(text), cached=False )