fix(chat): 流式问答中文乱码 + 问答模型链换成专用文字大模型

## 乱码修复
Gemini 流式接口 resp.iter_lines(decode_unicode=True) 没有显式设置
resp.encoding,requests 自己猜编码猜错了(Gemini 的 SSE 响应 Content-Type
不带 charset 参数),导致中文变成典型的 UTF-8 被当 Latin-1 解码的乱码。
fam-core 转发这一跳的 requests.iter_lines 调用有同样的坑,一并修了。
两处响应的 Content-Type 都补上 charset=utf-8,减少下游再犯同样错误的机会。
顺手用 latin-1/utf-8 往返把 NAS chat_history 里已经存了乱码的 3 条历史记录
恢复成正常文字。

## 问答模型链重新设计(跟视频分析完全独立)
用户要求问答不再用 flash/omni,优先用 NVIDIA 免费文字模型里上下文最大的几个,
不行再退 Gemini 非 flash 文字模型,最后本地 Ollama 兜底。

- base_adapter 新增 usage 字段(纯标记,不影响行为)
- qa.py 的 QAOrchestrator 只挑 role='text' 的适配器参与问答(按 config 出现
  顺序决定降级顺序),role='vision' 的视频分析适配器(gemini-flash-latest /
  nvidia omni)不再参与问答,两条链路彻底解耦
- 新增问答专用 NVIDIA 文字模型链(同一适配器内部 model_chain 降级):
  nemotron-3-ultra-550b-a55b(1M上下文/561B) -> nemotron-3-super-120b-a12b
  (1M上下文/124B) -> openai/gpt-oss-120b(131K上下文/117B)。三个都用真实
  API 调用验证过当前账号可访问;同时验证过 llama-3.1-70b-instruct 和
  nemotron-super-49b-v1.5 都收到"08/25/2026 起弃用"通知,排除;
  nemotron-ultra-253b/mistral-large-2/nemotron-4-340b/kimi-k2.6 在目录里
  能看到但实测调用返回 404"账号无权限",排除
- 新增问答专用 Gemini 非 flash 文字模型:gemini-pro-latest -> gemini-2.5-pro
  (1M 上下文,复用视频分析同一批多 key 轮换)
- /api/edge/chat/ask(/stream) 默认 max_tokens 从 1024 提到 3072:新链路
  优先用的都是"推理"模型,回答前会先吐一段思考过程,1024 经常在思考阶段
  就被截断,用户永远看不到真正答案

新增 test_qa.py::test_init_only_keeps_text_role_adapters_in_config_order
验证角色过滤+顺序正确。

本地检查过 Ollama 服务本身:systemd enabled+running(8h 正常运行,
qwen2.5:7b 已加载),并非没启动——用户观察到的现象另有原因。
This commit is contained in:
ericwyuan
2026-08-23 07:46:48 +08:00
parent 22bd4d4ff9
commit 9bba7b7e14
7 changed files with 130 additions and 9 deletions

View File

@@ -37,8 +37,11 @@ class BaseModelAdapter(ABC):
def __init__(self, provider_name: str, config: dict):
self.provider_name = provider_name # 如 "ollama", "gemini"
self.config = config
# 角色: vision=视觉分析, text=智能问答兜底(本地模型); 默认 vision
# 角色: vision=视觉分析, text=智能问答; 默认 vision
self.role = config.get('role', 'vision')
# usage: 纯文档/编排层筛选用的标记(如 "qa_primary"/"qa_fallback"
# 不影响本适配器自身行为QAOrchestrator 据此挑选参与问答链路的适配器。
self.usage = config.get('usage', '')
# 模型调用统计回调(由编排层注入):
# hook(provider, model, started_at, duration_sec, success, error)
self.model_call_hook = None

View File

@@ -494,6 +494,10 @@ class GeminiAdapter(BaseModelAdapter):
logger.warning(f"Gemini {key_label} [{model}] 流式问答 HTTP {resp.status_code}")
resp.close()
continue
# Gemini 响应体固定是 UTF-8但 Content-Type 没带 charset 参数,
# requests 会自己猜编码(猜错会把中文变成乱码)——强制指定,
# 不依赖 requests 的自动嗅探。
resp.encoding = 'utf-8'
try:
for line in resp.iter_lines(decode_unicode=True):
if not line or not line.startswith('data: '):