fix(langchain-chat): R1 思考过程显示 + 选题推荐放宽 + RAG 诊断日志

三个独立修复 / 排查:

1. R1 思考过程不显示
   - 根因: chat_test.py 等 <think> 开标签出现才进思考态,但 R1
     流式输出本来就在 reasoning 态启动,永远不出 <think>,所有
     reasoning 全部当 text 走到答案区
   - 修法: 引入 r1_thinking_done 状态机,默认在思考态,
     看到 </think> 切换;R1-70B 直连本地代理 deepseek-r1
     (官方 deepseek-reasoner 把 reasoning 放独立字段,旧版
     callback 取不到)
   - 结果验证: "1+1" → 269 think + 40 text,思考与答案正确分流

2. 选题推荐场景拒答 + chat 模板标记泄漏
   - 根因: prompt 写死了 "你只能回答有关选题推荐的问题"
     + 直接嵌入 <|im_start|>/<|im_end|> Qwen chat 标记
   - 修法: 改写 Topic Recommend Assistant prompt,删 chat 标记,
     行为准则改为"沾边查询用工具回答";agent_v2 增加 strip
     防守层
   - 结果验证: "钢铁行业研究重点方向" → agent 调工具,不再拒答

3. 知识库召回 0 排查(数据问题,未根治)
   - 根因: kb_config.py 把所有 KB(政策库/钢铁库/报告库等)
     都映射到 t_policy_total_bge_new_v2,但 Milvus 里根本没有
     这个 collection(实际只有 11 个 p_* 个人库 + 1 个
     t_journal_article_bge_v1)
   - 临时改: search_tool.py 加诊断日志 [RAG诊断] 输出每个 KB
     召回数;rag_search 内 for 循环里首个 KB 空就 return 的
     bug 改 continue
   - 待决策: kb_config 是否把默认 KB 映射到唯一存在的
     t_journal_article_bge_v1,或重建对应集合

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 15:44:05 +08:00
parent 316def2145
commit 846380879b
5 changed files with 55 additions and 30 deletions

View File

@@ -75,7 +75,7 @@ MAX_TOKENS = None
MAX_CUT_TOKENS = 30 * 1024 MAX_CUT_TOKENS = 30 * 1024
TEMPERATURE = 0.7 TEMPERATURE = 0.7
DEEPSEEK_MODELS = ["deepseek-reasoner", "deepseek-chat"] DEEPSEEK_MODELS = ["deepseek-r1", "deepseek-reasoner", "deepseek-chat"]
CAST_MODELS = ["kexie_0.5b"] CAST_MODELS = ["kexie_0.5b"]
ONLINE_LLM_MODEL = { ONLINE_LLM_MODEL = {
# 本地部署的大模型 API (10.102.24.75:3000) # 本地部署的大模型 API (10.102.24.75:3000)

View File

@@ -553,27 +553,27 @@ PROMPT_TEMPLATES = {
"Topic Recommend Assistant": "Topic Recommend Assistant":
'<角色> 你是由浪潮开发的选题推荐助手。</角色>\n\n' '<角色> 你是由浪潮开发的选题推荐助手。</角色>\n\n'
'<|im_start|>system 今天的日期为:{{time}} <|im_end|>' '今天的日期为:{{time}}\n'
'<选题样例>' '<选题样例>'
'**有色金属尾矿等大宗固废资源化及综合治理模式**' '**有色金属尾矿等大宗固废资源化及综合治理模式**'
'**机械产品的数字化设计与制造战略研究**' '**机械产品的数字化设计与制造战略研究**'
'**材料延寿与可持续发展战略研究综合报告**' '**材料延寿与可持续发展战略研究综合报告**'
'**民营科技企业创新机制研究**' '**民营科技企业创新机制研究**'
'**水产养殖业十四五"规划战略研究报告 **' '**水产养殖业"十四五"规划战略研究报告**'
'**污水资源化能源化的工程科技发展与战略研究污水资源化能源化的工程科技发展与战略研究报告**' '**污水资源化能源化的工程科技发展与战略研究综合报告**'
'**油价大幅波动情景下我国的油气勘探战略研究**' '**油价大幅波动情景下我国的油气勘探战略研究**'
'**洞庭湖大水脉研究咨询报告**' '**洞庭湖大水脉研究咨询报告**'
'**流程工业与循环经济**' '**流程工业与循环经济**'
'**流程工业装备绿色化、智能化与在役再制造“海上风电场建设重大工程问题研究"咨询项目研究结题报告 **' '**流程工业装备绿色化、智能化与在役再制造**'
'**海上风电场建设重大工程问题研究咨询项目研究结题报告**'
'</选题样例>\n' '</选题样例>\n'
'你的任务是根据user输入的方向或主题,依据多学科研究现状、前沿动态和发展趋势,为科研人员提供选题推荐。\n' '你的任务是根据用户输入的方向或主题,依据多学科研究现状、前沿动态和发展趋势,为科研人员提供选题推荐。\n'
'注意当user不明确的话只推荐一个选题\n' '注意:当用户问题不明确时,只推荐一个选题;当用户明确要求多个时,按要求数量给出。\n'
'重要指令】:如果用户的问题不是一个选题推荐的请求,禁止使用工具!!!然后你要给出友好回复,比如"抱歉,我无法回答这个问题"\n' '行为准则】:\n'
'【重要指令】:如果用户问题是一个选题推荐的请求,请使用工具!!!\n' '1. 用户问题"研究方向/选题/课题/报告/趋势/调研"等沾边时,使用工具检索资料后给出有帮助的回答。\n'
'【重要指令】:你只能回答有关选题推荐的问题!!!!!!如果回答了别的问题,会有十分恶劣的后果!!!!!\n' '2. 即使用户的问法不是"推荐选题"原话(如"写一份研究报告""有哪些研究重点"),也应理解为相关需求并提供选题建议或研究方向梳理。\n'
'现在开始:\n' '3. 仅当用户问题明显与科研选题/研究方向无关(如闲聊、生活问题、纯技术求解),才礼貌说明本助手专注于科研选题推荐。\n'
'<|im_start|>user {{input}} <|im_end|>' '用户问题:{{input}}\n'
'<|im_start|>assistant <|im_end|>\n'
, ,
"Topic Recommend Assistant_with_history": "Topic Recommend Assistant_with_history":

View File

@@ -111,9 +111,11 @@ def rag_search(query: str,uid):
knowledge_base_name=knownledge, knowledge_base_name=knownledge,
expr=expr_param expr=expr_param
) )
logging.info(f"[RAG诊断] kb={knownledge!r} expr={expr_param!r} 召回 {len(doc_list)} docs")
if len(doc_list)==0: if len(doc_list)==0:
return result,source_docs # 修 bug: 原代码 return 导致首个 KB 空就放弃全部 KB改 continue 继续尝试下一个
continue
titles = temp["title"] titles = temp["title"]
doc_list,title = utils.remove_docs1(titles,doc_list) doc_list,title = utils.remove_docs1(titles,doc_list)
titles.extend(title) titles.extend(title)
@@ -264,12 +266,15 @@ def search_tool(query: str):
result3 = [] result3 = []
# 获取结果 # 获取结果
result1,sourcedocs = future1.result() result1,sourcedocs = future1.result()
# 诊断:看 rag_search 实际召回多少
logging.info(f"[RAG诊断] rag_search 返回 result1={len(result1) if isinstance(result1, list) else type(result1).__name__}, sourcedocs={len(sourcedocs) if isinstance(sourcedocs, list) else type(sourcedocs).__name__}, kb={search.get('knowledge_name')}, query={search.get('query')!r}")
result2 = {} result2 = {}
if "type" in utils.get_shared_variable(time_based_uuid): if "type" in utils.get_shared_variable(time_based_uuid):
result2[0] =[] result2[0] =[]
result2[1] = [] result2[1] = []
else: else:
result2 = future2.result() result2 = future2.result()
logging.info(f"[RAG诊断] zhipu 返回 result2[0]={len(result2[0]) if isinstance(result2[0], list) else type(result2[0]).__name__}, result2[1]={len(result2[1]) if isinstance(result2[1], list) else type(result2[1]).__name__}")
# if "type" in utils.get_shared_variable(time_based_uuid): # if "type" in utils.get_shared_variable(time_based_uuid):
# result2[0] =[] # result2[0] =[]
# result2[1] = [] # result2[1] = []

View File

@@ -12,6 +12,7 @@ LangGraph 版 Agent runner。
import asyncio import asyncio
import json import json
import logging import logging
import re
from typing import AsyncIterable, List, Optional from typing import AsyncIterable, List, Optional
from langgraph.prebuilt import create_react_agent from langgraph.prebuilt import create_react_agent
@@ -26,10 +27,20 @@ from server.chat.tools_v2 import make_tools
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_CHAT_MARKER_RE = re.compile(r"<\|im_(?:start|end)\|>")
def _strip_chat_markers(text: str) -> str:
"""剥掉 prompt 内嵌的 Qwen chat template 标记,避免模型 echo 泄漏到答案。"""
return _CHAT_MARKER_RE.sub("", text or "")
def _build_system_prompt(user_prompt_name: str, query: str, think_content: str) -> str: def _build_system_prompt(user_prompt_name: str, query: str, think_content: str) -> str:
"""复用旧版 Think Test Bak + 用户业务 prompt 的拼装逻辑,但简化为单条 system message。""" """复用旧版 Think Test Bak + 用户业务 prompt 的拼装逻辑,但简化为单条 system message。"""
base = get_prompt_template("agent_chat", "Think Test Bak")
user = get_prompt_template("llm_chat", user_prompt_name) if user_prompt_name else "" user = get_prompt_template("llm_chat", user_prompt_name) if user_prompt_name else ""
user = _strip_chat_markers(user)
think_content = _strip_chat_markers(think_content)
parts = [] parts = []
parts.append("你是浪潮开发的智能专家。回答用户问题前可以使用工具检索资料。") parts.append("你是浪潮开发的智能专家。回答用户问题前可以使用工具检索资料。")
parts.append("严格要求:") parts.append("严格要求:")

View File

@@ -123,7 +123,9 @@ async def chat_test(
query = query if len(query)<20000 else TextRank(query,num_sentences=70) query = query if len(query)<20000 else TextRank(query,num_sentences=70)
query = query if len(query)<20000 else TextRank(query,num_sentences=10) query = query if len(query)<20000 else TextRank(query,num_sentences=10)
if model_name == "R1-70B": if model_name == "R1-70B":
model_name = DEEPSEEK_MODELS[0] # 本地代理 deepseek-r1 把 reasoning 放 content 里,能被 callback 捕获;
# 官方 deepseek-reasoner 把 reasoning 放独立的 reasoning_content 字段,旧版 langchain callback 取不到
model_name = "deepseek-r1"
elif model_name in ["QIANWEN", "Qwen1.5-32B-Chat"]: elif model_name in ["QIANWEN", "Qwen1.5-32B-Chat"]:
model_name = LLM_MODELS[0] model_name = LLM_MODELS[0]
if prompt_name == "customer_service": if prompt_name == "customer_service":
@@ -590,6 +592,8 @@ async def chat_test(
if stream: if stream:
menu = 0 #0处于deepseek思考过程中的状态1处于生成正文状态 menu = 0 #0处于deepseek思考过程中的状态1处于生成正文状态
include_think = False #是否包含思考(源码修改的手动拼接的思考标签) include_think = False #是否包含思考(源码修改的手动拼接的思考标签)
include_think1 = False
r1_thinking_done = False # R1: 见到 </think> 之前默认在思考态
async for token in callback.aiter(): async for token in callback.aiter():
if not utils.get_shared_variable(time_based_uuid)["status"]: if not utils.get_shared_variable(time_based_uuid)["status"]:
logging.info("\n==============================STOPPED==============================\n") logging.info("\n==============================STOPPED==============================\n")
@@ -633,18 +637,23 @@ async def chat_test(
yield json.dumps({"text": token}, ensure_ascii=False) yield json.dumps({"text": token}, ensure_ascii=False)
else: else:
if model_name in DEEPSEEK_MODELS: if model_name in DEEPSEEK_MODELS:
# R1 流式输出特点:默认从 reasoning 开始(不带 <think> 开标签),
# 看到 </think> 才切换到正式答案。
if not r1_thinking_done:
if "</think>" in token:
before, _, after = token.partition("</think>")
if before:
yield json.dumps({"think": before}, ensure_ascii=False)
r1_thinking_done = True
if after.strip():
yield json.dumps({"text": after}, ensure_ascii=False)
else:
# 兼容偶发输出 <think> 开标签的场景:剥掉后直接 yield think
if "<think>" in token: if "<think>" in token:
include_think = True
token = token.replace("<think>", "") token = token.replace("<think>", "")
logger.info(f"think:{token}") if token:
yield json.dumps({"think": token}, ensure_ascii=False) yield json.dumps({"think": token}, ensure_ascii=False)
else: else:
if menu == 1:
yield json.dumps({"text": token}, ensure_ascii=False)
if menu == 0 and include_think:
yield json.dumps({"text": token}, ensure_ascii=False)
menu = 1
if not include_think:
yield json.dumps({"text": token}, ensure_ascii=False) yield json.dumps({"text": token}, ensure_ascii=False)
else: else:
yield json.dumps( yield json.dumps(