Просмотр исходного кода

fix: 系统提示词INSERT列数不一致 + 知识库embedding TPM重试 + tags_str类型修复

Xiaogang Liao 2 месяцев назад
Родитель
Сommit
8cd80dad3b

+ 3 - 6
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -8750,18 +8750,15 @@ private void runMigration100() {
 				"('health_butler', '家庭健康管家', " +
 				"'你是一个家庭AI健康管家, 为用户提供专业、温暖的个性化健康建议。\\n你的能力:\\n1. 基于健康知识库(菌属功能、指标解读、营养素、食物)提供科学建议\\n2. 解读健康报告数据, 结合知识库给出改善方案\\n3. 根据菌群检测结果推荐精准营养方案\\n4. 提供可执行的健康微行动\\n回答原则:\\n1. 用中文, 像家庭医生一样亲切但专业\\n2. 引用知识库中的菌属/指标/食物信息作为证据\\n3. 优先使用知识库数据, 其次使用通用健康知识\\n4. 需要产品推荐时使用搜索工具\\n5. 生成 [TASK: {\"title\": \"任务名\", \"dimension\": \"身\", \"points\": 10, \"frequency\": \"每天\"}] 标记创建健康行动任务\\n6. 超出知识库范围的慢性病/急症问题, 建议咨询专业医生\\n7. 不要编造医学建议, 不夸大效果', " +
 				"'你是一个家庭AI健康管家, 为用户提供专业、温暖的个性化健康建议。\\n你的能力:\\n1. 基于健康知识库(菌属功能、指标解读、营养素、食物)提供科学建议\\n2. 解读健康报告数据, 结合知识库给出改善方案\\n3. 根据菌群检测结果推荐精准营养方案\\n4. 提供可执行的健康微行动\\n回答原则:\\n1. 用中文, 像家庭医生一样亲切但专业\\n2. 引用知识库中的菌属/指标/食物信息作为证据\\n3. 优先使用知识库数据, 其次使用通用健康知识\\n4. 需要产品推荐时使用搜索工具\\n5. 生成 [TASK: {\"title\": \"任务名\", \"dimension\": \"身\", \"points\": 10, \"frequency\": \"每天\"}] 标记创建健康行动任务\\n6. 超出知识库范围的慢性病/急症问题, 建议咨询专业医生\\n7. 不要编造医学建议, 不夸大效果', " +
-				"1, 'AI健康管家对话', " +
-				"'init'), " +
+				"1, 'AI健康管家对话'), " +
 				"('health_coach', '儿童健康教练', " +
 				"'你是一个儿童健康教练,喜欢用提问的方式引导家长发现问题和解决方案。\\n核心原则:\\n1. 先问后答:不要直接给答案,用问题引导家长思考\\n2. 用开放式问题了解情况,再给针对性建议\\n3. 语气温暖,像一个耐心的教练在陪伴家长成长\\n你的职责:\\n1. 儿童体质调理(如挑食、瘦弱、易疲劳、反复生病)—— 用提问了解情况\\n2. 科学喂养和营养建议 —— 先问饮食现状再给建议\\n3. 日常保健(睡眠、运动、季节防护)—— 引导家长发现生活中的问题\\n4. 体质辨识基础咨询 —— 通过提问帮助家长理解孩子体质特点\\n5. 健康问题判断 —— 先收集症状信息再建议是否就医\\n回答结构: 提问-分析-建议-跟进', " +
 				"'你是一个儿童健康教练,喜欢用提问的方式引导家长发现问题和解决方案。\\n核心原则:\\n1. 先问后答:不要直接给答案,用问题引导家长思考\\n2. 用开放式问题了解情况,再给针对性建议\\n3. 语气温暖,像一个耐心的教练在陪伴家长成长\\n你的职责:\\n1. 儿童体质调理(如挑食、瘦弱、易疲劳、反复生病)—— 用提问了解情况\\n2. 科学喂养和营养建议 —— 先问饮食现状再给建议\\n3. 日常保健(睡眠、运动、季节防护)—— 引导家长发现生活中的问题\\n4. 体质辨识基础咨询 —— 通过提问帮助家长理解孩子体质特点\\n5. 健康问题判断 —— 先收集症状信息再建议是否就医\\n回答结构: 提问-分析-建议-跟进', " +
-				"1, '儿童健康咨询对话', " +
-				"'init'), " +
+				"1, '儿童健康咨询对话'), " +
 				"('chat_assistant', '儿童成长助手', " +
 				"'你是一个儿童成长家庭助手, 回答关于孩子成长、健康、教育的各种问题。', " +
 				"'你是一个儿童成长家庭助手, 回答关于孩子成长、健康、教育的各种问题。', " +
-				"1, '通用对话助手', " +
-				"'init')"
+				"1, '通用对话助手')"
 			);
 			log.info("已插入默认系统提示词");
 		} catch (Exception e) {

+ 36 - 5
cfc-langgraph/app/rag/embeddings.py

@@ -1,8 +1,12 @@
 from typing import List
+import time
+import logging
 from langchain_core.embeddings import Embeddings
-from openai import OpenAI
+from openai import OpenAI, RateLimitError
 from app.config import settings
 
+logger = logging.getLogger(__name__)
+
 
 class SiliconFlowEmbeddings(Embeddings):
     """OpenAI 兼容 embeddings 直连客户端(绕开 langchain tiktoken token 化)
@@ -10,19 +14,46 @@ class SiliconFlowEmbeddings(Embeddings):
     langchain 的 OpenAIEmbeddings 默认 tiktoken_enabled=True,会把文本编码成
     整数 token 数组发送给 API(OpenAI 官方接受,但 siliconflow 等第三方网关
     只接受字符串 input,导致 400 code:20015 parameter invalid)。
-    本类直接用 openai SDK 发送字符串数组,兼容 siliconflow / siliconflow 等网关。
+    本类直接用 openai SDK 发送字符串数组,兼容 siliconflow 等网关。
+
+    内置 429 TPM 指数退避重试:SiliconFlow 全量同步易触 TPM 限额,429 是
+    临时性状态,退避重试能自动恢复完成,避免 sync 整段中断。
     """
 
+    _BATCH_SIZE = 32
+    _THROTTLE_SECONDS = 0.02
+    _MAX_RETRY = 3
+    _INITIAL_RETRY_SECONDS = 2.0
+
     def __init__(self, model: str, api_key: str, base_url: str):
         self.model = model
         self.client = OpenAI(api_key=api_key, base_url=base_url)
 
+    def _call_with_retry(self, batch):
+        wait = self._INITIAL_RETRY_SECONDS
+        for attempt in range(self._MAX_RETRY + 1):
+            try:
+                return self.client.embeddings.create(model=self.model, input=batch)
+            except RateLimitError as e:
+                if attempt == self._MAX_RETRY:
+                    raise
+                logger.warning("embeddings 429 TPM 限制,%.0fs 后重试 (%d/%d)", wait, attempt + 1, self._MAX_RETRY)
+                time.sleep(wait)
+                wait = min(wait * 2, 30.0)
+        raise RuntimeError("unreachable")
+
     def embed_documents(self, texts: List[str]) -> List[List[float]]:
-        resp = self.client.embeddings.create(model=self.model, input=texts)
-        return [d.embedding for d in resp.data]
+        result = []
+        for i in range(0, len(texts), self._BATCH_SIZE):
+            batch = texts[i:i + self._BATCH_SIZE]
+            resp = self._call_with_retry(batch)
+            result.extend(d.embedding for d in resp.data)
+            if i + self._BATCH_SIZE < len(texts):
+                time.sleep(self._THROTTLE_SECONDS)
+        return result
 
     def embed_query(self, text: str) -> List[float]:
-        resp = self.client.embeddings.create(model=self.model, input=[text])
+        resp = self._call_with_retry([text])
         return resp.data[0].embedding
 
 

+ 1 - 1
cfc-langgraph/app/rag/retriever.py

@@ -44,7 +44,7 @@ class RagRetriever:
                     "id": str(a["id"]),
                     "title": title,
                     "summary": a.get("summary", ""),
-                    "tags": tags,
+                    "tags": tags_str,
                     "type": "article",
                 })