同步46服务器代码并修复RAG溯源合并

This commit is contained in:
ChenRui 2026-07-22 17:17:32 +08:00
parent e242d9caa8
commit e84cd1f2aa
14 changed files with 1497 additions and 1338 deletions

16
app.py
View File

@ -31,6 +31,7 @@ from workflow_registry import WORKFLOW_CONFIG, VALID_ROUTE_FLAGS
from main_agent import extract_conversation_title from main_agent import extract_conversation_title
from workflows.history_manager import filter_image_urls, preprocess_from_request from workflows.history_manager import filter_image_urls, preprocess_from_request
from config import set_request_user_config from config import set_request_user_config
from utils.source_citations import merge_rag_source_citations
from checkpointer_config import ( from checkpointer_config import (
CheckpointerManager, CheckpointerManager,
checkpointer_manager checkpointer_manager
@ -462,7 +463,7 @@ async def stream_main_agent_execution(
agent_task = asyncio.create_task(run_agent()) agent_task = asyncio.create_task(run_agent())
execution_complete_received = False execution_complete_received = False
rag_result = [] rag_result = {}
graph_result = [] graph_result = []
while True: while True:
try: try:
@ -482,16 +483,7 @@ async def stream_main_agent_execution(
if title == "知识库搜索工具" and result and isinstance(result, dict): if title == "知识库搜索工具" and result and isinstance(result, dict):
print("===============================", result) print("===============================", result)
rag_result_raw = result.get("sourceCitation", {}) rag_result_raw = result.get("sourceCitation", {})
rag_result = {} merge_rag_source_citations(rag_result, rag_result_raw)
for doc_name, chunks in rag_result_raw.items():
if isinstance(chunks, list):
rag_result[doc_name] = [
{k: v for k, v in item.items() if k != "text"}
if isinstance(item, dict) else item
for item in chunks
]
else:
rag_result[doc_name] = chunks
elif title == "图谱检索工具" and result and isinstance(result, dict): elif title == "图谱检索工具" and result and isinstance(result, dict):
print("===============================", result) print("===============================", result)
graph_result = result.get("xxxx.graph", []) graph_result = result.get("xxxx.graph", [])
@ -1017,5 +1009,3 @@ if __name__ == "__main__":

View File

@ -220,3 +220,13 @@ NEO4J_CONFIG = {
"user": "neo4j", "user": "neo4j",
"password": "zdht123@" "password": "zdht123@"
} }
# ==================== Rerank 服务配置 ====================
RERANK_CONFIG = {
"enabled": "true".lower() == "true",
"endpoint": "http://192.168.0.46:59600/v1/rerank",
"model": "bge-rerank",
"timeout": 30.0,
"max_candidates": 24,
"top_k": 10,
}

View File

@ -4,7 +4,7 @@ from typing import List, Union
import os import os
import numpy as np import numpy as np
from openai import OpenAI from openai import OpenAI
from config import LLM_CONFIG, EMBEDDING_CONFIG from config import LLM_CONFIG, EMBEDDING_CONFIG, RERANK_CONFIG
from neo4j_graphrag.embeddings.base import Embedder from neo4j_graphrag.embeddings.base import Embedder
from openai import AsyncOpenAI from openai import AsyncOpenAI
@ -24,6 +24,25 @@ def _get_async_client() -> AsyncOpenAI:
return _async_client return _async_client
def _log_model_messages(label: str, model: str, message_list: list) -> None:
if os.getenv("LLM_LOG_FULL_PROMPTS", "false").lower() == "true":
print(label, message_list)
return
if os.getenv("LLM_LOG_CALLS", "true").lower() != "true":
return
summary = []
total_chars = 0
for message in message_list or []:
content = message.get("content") if isinstance(message, dict) else ""
if isinstance(content, str):
chars = len(content)
else:
chars = len(str(content))
total_chars += chars
summary.append({"role": message.get("role", ""), "chars": chars})
print(label, {"model": model, "messages": summary, "total_chars": total_chars})
class OpenaiAPI: class OpenaiAPI:
@staticmethod @staticmethod
async def open_api_chat_without_thinking( async def open_api_chat_without_thinking(
@ -64,7 +83,7 @@ class OpenaiAPI:
if json_output: if json_output:
kwargs["response_format"] = {"type": "json_object"} kwargs["response_format"] = {"type": "json_object"}
print("model API. history message", message_list) _log_model_messages("model API call", model, message_list)
response = await client.chat.completions.create(**kwargs) response = await client.chat.completions.create(**kwargs)
return response.choices[0].message.content return response.choices[0].message.content
@ -112,7 +131,7 @@ class OpenaiAPI:
if json_output: if json_output:
kwargs["response_format"] = {"type": "json_object"} kwargs["response_format"] = {"type": "json_object"}
print("model API stream. history message", message_list) _log_model_messages("model API stream call", model, message_list)
stream = await client.chat.completions.create(**kwargs) stream = await client.chat.completions.create(**kwargs)
async for chunk in stream: async for chunk in stream:
@ -224,6 +243,66 @@ class OpenaiAPI:
embedding = response.json()["data"][0]["embedding"] embedding = response.json()["data"][0]["embedding"]
return embedding return embedding
@staticmethod
async def rerank(
query: str,
documents: List[str],
top_k: int = None,
model: str = None,
) -> List[dict]:
"""
调用 59600 rerank 服务
返回格式:
[
{"index": 0, "relevance_score": 0.98},
...
]
"""
if not RERANK_CONFIG.get("enabled", True) or not query or not documents:
return []
max_candidates = int(RERANK_CONFIG.get("max_candidates") or 24)
selected_documents = [str(x or "")[:4000] for x in documents[:max_candidates]]
try:
async with httpx.AsyncClient(
timeout=float(RERANK_CONFIG.get("timeout") or 30),
verify=False,
trust_env=False,
) as client:
response = await client.post(
str(RERANK_CONFIG["endpoint"]),
json={
"model": model or RERANK_CONFIG.get("model") or "bge-rerank",
"query": query,
"documents": selected_documents,
},
)
response.raise_for_status()
payload = response.json()
except Exception as exc:
print(f"[rerank] rerank service failed: {exc}")
return []
rows: List[dict] = []
for row in payload.get("results", []):
try:
idx = int(row.get("index"))
except Exception:
continue
if idx < 0 or idx >= len(selected_documents):
continue
rows.append({
"index": idx,
"relevance_score": float(row.get("relevance_score") or 0.0),
})
rows.sort(key=lambda x: float(x.get("relevance_score") or 0), reverse=True)
if top_k is not None:
rows = rows[:top_k]
return rows
@staticmethod @staticmethod
async def get_embeddings_batch_async(texts: List[str]) -> List[List[float]]: async def get_embeddings_batch_async(texts: List[str]) -> List[List[float]]:
embedding_base_url = EMBEDDING_CONFIG["base_url"] embedding_base_url = EMBEDDING_CONFIG["base_url"]
@ -384,3 +463,4 @@ if __name__ == "__main__":

View File

@ -427,29 +427,19 @@ BAIKE_PROMPTS = {
# 输入变量: history_messages, original_query # 输入变量: history_messages, original_query
# ------------------------------------------------------------------ # ------------------------------------------------------------------
"judge_need_retrieval": ( "judge_need_retrieval": (
"你是一个专业的问题分析助手,请判断用户的问题是否需要检索知识库才能回答。\n" "You are an intent classifier for a knowledge-base QA assistant. Decide whether the user question needs retrieval.\n"
"\n" "\n"
"【判断标准】\n" "Return true when the question asks about documents, files, knowledge-base content, equipment, systems, faults, operations, standards, procedures, meeting notes, database-related materials, or any factual/domain-specific information.\n"
"需要检索的情况:\n" "Return false only for pure greetings, thanks, cancellation/confirmation with no substantive question, or a follow-up that can be fully answered from the conversation history alone.\n"
"- 询问特定设备、技术、规程、规范的具体内容\n" "Default rule: when unsure, return true.\n"
"- 需要专业知识或特定数据支持的问题\n"
"- 询问故障诊断、维修方法等专业领域问题\n"
"- 询问历史事件、法规条款、技术标准等需要查证的内容\n"
"- 例如发动机的组成等\n"
"\n" "\n"
"不需要检索的情况:\n" "Conversation history:\n"
"- 简单的问候、寒暄\n"
"- 常识性问题(如常识性知识、简单计算等)\n"
"- 明确不需要专业知识的问题\n"
"- 对之前对话的简单追问,上下文已足够回答\n"
"\n"
"【对话历史】\n"
"{history_messages}\n" "{history_messages}\n"
"\n" "\n"
"【当前用户问题】\n" "User question:\n"
"{original_query}\n" "{original_query}\n"
"\n" "\n"
"请仅输出 \"true\"\"false\"不带引号true 表示需要检索false 表示不需要检索。" "Output only true or false."
), ),
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -457,31 +447,35 @@ BAIKE_PROMPTS = {
# 输入变量: last_user_query, history_str, original_query # 输入变量: last_user_query, history_str, original_query
# ------------------------------------------------------------------ # ------------------------------------------------------------------
"rewrite_query_with_history": ( "rewrite_query_with_history": (
"你是一个专业的检索query重写助手负责根据对话历史和当前用户问题生成**适合向量检索/RAG 的单条查询语句**。\n" "You are a query understanding assistant for a Chinese knowledge-base retrieval system.\n"
"\n" "\n"
"请严格遵守以下要求:\n" "Tasks:\n"
"1. 只融合与当前问题语义强相关的**最近几轮对话信息**,不要机械拼接全部历史。\n" "1. Rewrite the current user question into a self-contained Chinese retrieval query.\n"
"2. 消除指代和省略(例如:这个、刚才那个问题、上面说的故障等),补全成自洽、完整的描述。\n" "2. Generate 2-5 short search queries for hybrid retrieval.\n"
"3. 不要加入与用户问题无关的背景信息,不要过度扩展检索范围。\n"
"4. 输出必须是一句自然的中文问句或陈述句,便于检索;不要包含条目符号、编号或解释性文字。\n"
"5. 只输出改写后的query本身\n"
" - 不要输出说明文字\n"
" - 不要包含「改写结果」「检索query」等前缀\n"
" - 不要添加引号\n"
"\n" "\n"
"【特别提示】\n" "Rules:\n"
"- 请优先参考**上一轮用户的问题**来重写本轮query。\n" "- Resolve references and ellipsis using only relevant recent history.\n"
"- 上一轮用户的问题是:{last_user_query}\n" "- Preserve concrete entities, file names, database/table terms, equipment names, fault names, standards, procedure names, and exact keywords.\n"
"- Do not output meta instructions such as “在知识库中查找”. Output the actual search content.\n"
"- Include compact keyword queries that help exact matching, for example document titles, equipment names, or core nouns.\n"
"- The rewritten query and search queries must be in Chinese unless the original keyword is English.\n"
"\n" "\n"
"【对话历史JSON 格式,已按时间排序)】\n" "Last user question:\n"
"{last_user_query}\n"
"\n"
"Conversation history JSON:\n"
"{history_str}\n" "{history_str}\n"
"\n" "\n"
"【当前用户问题】\n" "Current user question:\n"
"{original_query}\n" "{original_query}\n"
"\n" "\n"
"请给出最终用于检索的单条中文query。" "Output ONLY valid JSON:\n"
"{{\"rewrite_query\":\"string\",\"search_queries\":[\"string\"]}}"
), ),
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# generate_suggested_replies_baike 函数:生成百科问答的建议回复问题 # generate_suggested_replies_baike 函数:生成百科问答的建议回复问题
# 输入变量: question, answer # 输入变量: question, answer
@ -514,8 +508,15 @@ BAIKE_PROMPTS = {
# generate_answer 节点 - 有检索结果时的系统提示词 # generate_answer 节点 - 有检索结果时的系统提示词
# 输入变量: domain_desc # 输入变量: domain_desc
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# ------------------------------------------------------------------
"generate_answer_with_retrieval_system": ( "generate_answer_with_retrieval_system": (
"我是一个专业的{domain_desc}知识问答助手,根据用户的问题综合检索结果,提供准确、专业、完整的回答。回答应围绕问题组织知识点、依据和必要图示,把参考资料中的相关图片作为资料证据放在对应说明附近。只能使用参考资料中真实存在的图片链接,不得使用占位链接或自行构造图片链接。" "You are a professional knowledge-base assistant for {domain_desc}.\n"
"Answer ONLY from the retrieved evidence provided by the user message. Do not use prior knowledge as factual evidence.\n"
"If the evidence is insufficient, say clearly what is missing and what can be confirmed from the evidence.\n"
"Prefer concrete facts, names, parameters, steps, conclusions, and source relationships from the evidence.\n"
"When sources conflict, explain the conflict instead of inventing a merged fact.\n"
"Use images only when their URLs appear in the retrieved evidence; never fabricate or repair image URLs.\n"
"Always respond in Chinese. Do not expose internal tool names or implementation details."
), ),
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -523,24 +524,24 @@ BAIKE_PROMPTS = {
# 输入变量: extracted_text, rag_count, all_source_text # 输入变量: extracted_text, rag_count, all_source_text
# ------------------------------------------------------------------ # ------------------------------------------------------------------
"generate_answer_with_retrieval_user": ( "generate_answer_with_retrieval_user": (
"用户问题:\n"
"{extracted_text}\n" "{extracted_text}\n"
"\n" "\n"
"# 检索到的相关信息(共 {rag_count} 条):\n" "# Retrieved evidence ({rag_count} items)\n"
"\n" "\n"
"{all_source_text}\n" "{all_source_text}\n"
"\n" "\n"
"【输出要求】\n" "【输出要求】\n"
"1. 先直接回答用户问题,再按需要补充结构、原理、参数、应用场景、注意事项或资料依据\n" "1. 先直接回答用户问题,再补充必要依据。\n"
"2. 参考资料中真实存在的图片可在当前问题、设备结构、部件位置、流程步骤或现象说明对应段落附近自然展示;图片规则不要写入正文\n" "2. 每个关键事实都必须能在 Retrieved evidence 中找到依据;不要补充资料外结论。\n"
"3. 图片只作为资料引用输出为单个 Markdown 图片标签URL 和文件名必须逐字来自参考资料,不要改写、拆分、补全、连续重复或自行构造;同一图片资源重复出现时只引用一次\n" "3. 参考资料中真实存在的图片可在对应段落附近展示URL 必须逐字来自资料。\n"
"4. 如果参考资料包含 LaTeX 公式、链接等,请在对应位置正常使用,不得修改或删除\n" "4. 表格内容请转成自然语言描述,不要输出复杂表格。\n"
"5. 请专注于内容的准确性、完整性和逻辑性,不要担心格式的美观性\n"
"6. 可以适当使用简单的段落分隔但不要使用复杂的Markdown格式\n"
"7. 如果参考资料包含表格,请不要输出表格,而要直接描述表格的内容\n"
"\n" "\n"
"请给出回复:" "请给出回复:"
), ),
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# generate_answer 节点 - 无检索结果时的系统提示词 # generate_answer 节点 - 无检索结果时的系统提示词
# 输入变量: domain_desc # 输入变量: domain_desc

View File

@ -536,6 +536,7 @@ def _node_record_to_dict(record: Any) -> Dict[str, Any]:
return result return result
@track_function_calls
async def normalize_device_name_by_ship_graph( async def normalize_device_name_by_ship_graph(
device_name: str, device_name: str,
fulltext_index: str = GLOBAL_FULLTEXT_INDEX, fulltext_index: str = GLOBAL_FULLTEXT_INDEX,
@ -902,6 +903,7 @@ def _build_fault_graph_results_text(
return "\n".join(parts) return "\n".join(parts)
@track_function_calls
async def find_system_by_device( async def find_system_by_device(
device_name: str, device_name: str,
min_hops: int = 2, min_hops: int = 2,

71
utils/source_citations.py Normal file
View File

@ -0,0 +1,71 @@
"""Utilities for accumulating RAG source citations across multiple searches."""
import json
from typing import Any, Dict
def _citation_item_key(item: Any) -> str:
"""Build a stable identity for one RAG citation chunk."""
if not isinstance(item, dict):
return json.dumps(item, ensure_ascii=False, sort_keys=True, default=str)
chunk_id = item.get("id")
if chunk_id not in (None, ""):
return f"id:{chunk_id}"
location = {
"file_id": item.get("file_id"),
"page_idx": item.get("page_idx"),
"positions": item.get("positions"),
}
if any(value not in (None, "", []) for value in location.values()):
return "location:" + json.dumps(
location, ensure_ascii=False, sort_keys=True, default=str
)
# index/score may differ between rewritten queries and do not identify a source.
fallback = {
key: value
for key, value in item.items()
if key not in {"index", "score", "text"}
}
return "fallback:" + json.dumps(
fallback, ensure_ascii=False, sort_keys=True, default=str
)
def merge_rag_source_citations(
accumulated: Dict[str, Any], incoming: Any
) -> Dict[str, Any]:
"""
Append a RAG call's citations without losing earlier calls or duplicating chunks.
Citation text is intentionally removed here because the stream response has never
exposed it; the full text remains available inside the workflow for answer generation.
"""
if not isinstance(incoming, dict) or not incoming:
return accumulated
for doc_name, chunks in incoming.items():
if isinstance(chunks, list):
target = accumulated.setdefault(doc_name, [])
if not isinstance(target, list):
# Be defensive about malformed/legacy data while preserving both values.
target = [target]
accumulated[doc_name] = target
seen = {_citation_item_key(item) for item in target}
for item in chunks:
sanitized_item = (
{key: value for key, value in item.items() if key != "text"}
if isinstance(item, dict)
else item
)
item_key = _citation_item_key(sanitized_item)
if item_key not in seen:
target.append(sanitized_item)
seen.add(item_key)
elif doc_name not in accumulated:
accumulated[doc_name] = chunks
return accumulated

View File

@ -1,546 +1,544 @@
""" """
工作流普通问答Agent使用LangGraph 百科问答工作流
输入文本转化后和原文本query图片描述语音文本
步骤 核心链路借鉴 WeKnora
1. 使用RAG和GraphRAG检索相关信息 1. 意图判断默认偏向检索
2. 生成回答 2. 基于历史重写检索 query并生成多个检索变体
使用 OpenaiAPI 方法调用大模型尽可能使用异步 3. RAG / Graph 并行召回
4. 多源 evidence 去重融合排序rerank
5. 只基于 evidence 生成可追溯回答
""" """
import sys
import os
# 添加项目根目录到Python搜索路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from typing import TypedDict, Optional, Dict, Any, List
from langgraph.graph import StateGraph, START, END
from workflows.history_manager import init_history, DEFAULT_HISTORY_MAX_MESSAGES
# from workflows.workflow_utils import stream_generate_and_postprocess
from workflows.workflow_utils import dedupe_rag_results, stream_generate_and_postprocess
from tools.function_tool import (
graph_rag_search
)
from tools.rag_tools import rag_search
from modelsAPI.model_api import OpenaiAPI
from utils.function_tracker import track_function_calls, get_all_callbacks
from utils.image_utils import extract_image_paths
from prompts import BAIKE_PROMPTS
import asyncio import asyncio
import json import json
import re import os
import random import random
import re
import sys
from typing import Any, Dict, List, Optional, TypedDict
from langgraph.graph import END, START, StateGraph
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from modelsAPI.model_api import OpenaiAPI
from prompts import BAIKE_PROMPTS
from tools.function_tool import graph_rag_search
from tools.rag_tools import rag_search
from utils.function_tracker import get_all_callbacks, track_function_calls
from utils.image_utils import extract_image_paths
from workflows.history_manager import DEFAULT_HISTORY_MAX_MESSAGES, init_history
from workflows.workflow_utils import dedupe_rag_results, stream_generate_and_postprocess
BAIKE_RAG_TOP_K = 8 BAIKE_RAG_TOP_K = 8
BAIKE_GRAPH_TOP_K = 240 BAIKE_GRAPH_TOP_K = 240
BAIKE_RESULT_LIMIT = 8 BAIKE_RESULT_LIMIT = 8
BAIKE_EVIDENCE_LIMIT = 10
# =============== 定义状态 ===============
class QAState(TypedDict): class QAState(TypedDict):
"""普通问答工作流状态""" extracted_text: str
extracted_text: str # 已提取的文本(统一接口,只接收文本)
combined_query: str combined_query: str
retrieval_query: str # 用于RAG/图谱检索的重写后query retrieval_query: str
route_flag: str # 路由标识 retrieval_queries: List[str]
history_message: str # 历史消息JSON格式 route_flag: str
history_messages: List[Dict[str, Any]] # 过滤后的历史消息列表 history_message: str
need_retrieval: Optional[bool] # 是否需要检索 history_messages: List[Dict[str, Any]]
need_retrieval: Optional[bool]
rag_search_result: Optional[List[Dict[str, Any]]]
graph_search_result: Optional[str]
evidence_items: Optional[List[Dict[str, Any]]]
response: str
actions: List[Dict[str, Any]]
suggestedReplies: List[Dict[str, Any]]
error_message: str
# RAG检索结果
rag_search_result: Optional[List[Dict[str, Any]]] # RAG检索结果
graph_search_result: Optional[str] # 图谱检索结果
# 最终响应
response: str # 最终响应
actions: List[Dict[str, Any]] # 按钮信息列表 [{"type": "...", "label": "...", "icon": "..."}]
suggestedReplies: List[Dict[str, Any]] # 建议回复问题列表
error_message: str # 错误信息
# =============== 节点函数 ===============
def get_baike_history_context(state: QAState) -> Dict[str, Any]: def get_baike_history_context(state: QAState) -> Dict[str, Any]:
""" return init_history(state.get("history_message", "") or "")
知识问答步骤获取问答类历史信息
问答工作流只关注历史问答记录用于提供上下文
"""
history_message = state.get("history_message", "") or ""
return init_history(history_message)
async def judge_need_retrieval(state: QAState) -> Dict[str, Any]: async def judge_need_retrieval(state: QAState) -> Dict[str, Any]:
"""
判断用户问题是否需要检索知识库
"""
original_query = (state.get("combined_query") or state.get("extracted_text") or "").strip() original_query = (state.get("combined_query") or state.get("extracted_text") or "").strip()
history_messages = state.get("history_messages", []) or [] history_messages = state.get("history_messages", []) or []
prompt = BAIKE_PROMPTS["judge_need_retrieval"].format( prompt = BAIKE_PROMPTS["judge_need_retrieval"].format(
history_messages=history_messages if history_messages else '', history_messages=history_messages if history_messages else "",
original_query=original_query original_query=original_query,
) )
need_retrieval = True substantive_query = _looks_like_substantive_query(original_query)
need_retrieval = substantive_query
try: try:
result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None) result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None)
if result: if not substantive_query and result and result.strip().lower() == "false":
result = result.strip().lower() need_retrieval = False
if result == "false": except Exception as exc:
need_retrieval = False print(f"判断是否需要检索失败: {exc}")
except Exception as e:
print(f"判断是否需要检索失败: {e}")
need_retrieval = True
print(f"是否需要检索: {need_retrieval}") print(f"是否需要检索: {need_retrieval}")
return {"need_retrieval": need_retrieval} return {"need_retrieval": need_retrieval}
def _looks_like_substantive_query(query: str) -> bool:
text = re.sub(r"\s+", "", (query or "").strip())
if not text:
return False
if re.fullmatch(r"(你好|您好|hi|hello|在吗|谢谢|多谢|好的|好|嗯|哦|确认|取消|不用了)[。!!?\s]*", text, re.I):
return False
if len(text) >= 4:
return True
return bool(re.search(r"[?]|[A-Za-z0-9]{2,}", text))
async def rewrite_query_with_history(state: QAState) -> Dict[str, Any]: async def rewrite_query_with_history(state: QAState) -> Dict[str, Any]:
"""
使用历史上下文对当前问题进行query重写生成适合RAG/图谱检索的独立查询语句
只让大模型在有限的最近历史中挑选与当前问题语义相关的内容进行融合避免简单拼接全量历史
"""
import json
# 原始问题:优先使用 combined_query其次使用 extracted_text
original_query = (state.get("combined_query") or state.get("extracted_text") or "").strip() original_query = (state.get("combined_query") or state.get("extracted_text") or "").strip()
history_messages = state.get("history_messages", []) or [] history_messages = state.get("history_messages", []) or []
trimmed_history = history_messages[-DEFAULT_HISTORY_MAX_MESSAGES:] if history_messages else [] trimmed_history = history_messages[-DEFAULT_HISTORY_MAX_MESSAGES:] if history_messages else []
history_str = _json_dumps(trimmed_history)
try:
history_str = json.dumps(trimmed_history, ensure_ascii=False)
except Exception:
history_str = str(trimmed_history)
# 如果没有历史或当前问题过短,就直接跳过重写
if not original_query:
return {"retrieval_query": original_query}
# 在构造 prompt 前,提取上一轮用户 query如果有
last_user_query = "" last_user_query = ""
if trimmed_history: for msg in reversed(trimmed_history):
# 从后往前找最近一条 role == "user" 的消息 if msg.get("role") == "user":
for msg in reversed(trimmed_history): last_user_query = str(msg.get("content") or "").strip()
if msg.get("role") == "user": break
last_user_query = msg.get("content", "").strip()
break if not original_query:
return {"retrieval_query": "", "retrieval_queries": []}
prompt = BAIKE_PROMPTS["rewrite_query_with_history"].format( prompt = BAIKE_PROMPTS["rewrite_query_with_history"].format(
last_user_query=last_user_query or '(无)', last_user_query=last_user_query or "",
history_str=history_str, history_str=history_str,
original_query=original_query original_query=original_query,
) )
rewritten_query = original_query rewritten_query = original_query
query_variants: List[str] = []
try: try:
result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None) raw = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None, json_output=True)
if result: data = _parse_json_object(raw)
candidate = result.strip().strip("“”\"'") if data:
# 只在模型返回的内容明显为单行短文本时替换,避免异常输出 candidate = str(data.get("rewrite_query") or "").strip()
if candidate and len(candidate) <= 200 and "\n" not in candidate: if candidate:
rewritten_query = candidate rewritten_query = candidate[:180]
except Exception as e: for q in data.get("search_queries") or []:
print(f"query 重写失败: {e}") if isinstance(q, str) and q.strip():
query_variants.append(q.strip()[:180])
except Exception as exc:
title_options = [f"✨正在分析中,请稍等..."] print(f"query 重写失败: {exc}")
start_event = {
"type": "function_execution",
"title": random.choice(title_options),
"details": f"正在调取百科数据..."
}
for callback in get_all_callbacks():
try:
callback(start_event)
except Exception:
pass
query_variants = _build_search_queries(rewritten_query, original_query, query_variants)
_emit_event("✨正在分析中,请稍等...", "正在生成检索策略...")
print("原始检索query:", original_query) print("原始检索query:", original_query)
print("重写后检索query:", last_user_query+rewritten_query) print("重写后检索query:", rewritten_query)
print("检索query变体:", query_variants)
return {"retrieval_query":last_user_query+rewritten_query} return {"retrieval_query": rewritten_query, "retrieval_queries": query_variants}
@track_function_calls @track_function_calls
async def search_rag_and_graph(state: QAState) -> Dict[str, Any]: async def search_rag_and_graph(state: QAState) -> Dict[str, Any]:
""" kb_id = "3" if state.get("route_flag") == "rules" else None
进行文本知识检索和图谱知识检索 query = (state.get("retrieval_query") or state.get("combined_query") or state.get("extracted_text") or "").strip()
""" extracted_text = (state.get("extracted_text") or "").strip()
kb_id = None query_variants = _build_search_queries(query, extracted_text, state.get("retrieval_queries") or [])
if state.get("route_flag") == "rules":
kb_id = '3'
# 优先使用重写后的检索query其次使用 combined_query最后回退到原始 extracted_text
query = state.get("retrieval_query") or state.get("combined_query") or state.get("extracted_text", "")
if "用户问题:" in query:
query = query.split("用户问题:", 1)[1]
print("Graph 检索query", query)
extracted_text = state.get("extracted_text", "")
if "用户问题:" in extracted_text:
extracted_text = extracted_text.split("用户问题:", 1)[1]
try: try:
# 异步并行调用RAG检索和图谱检索 rag_task = _search_rag_variants(query_variants[:3], kb_id=kb_id)
print(kb_id) graph_task = graph_rag_search.ainvoke({"query": query, "top_k": BAIKE_GRAPH_TOP_K})
print(33333333333333333333333333333333333333) rag_results, graph_results = await asyncio.gather(rag_task, graph_task)
evidence_items = _build_evidence_items(
async def _do_rag_search(): rag_results=rag_results,
if kb_id: graph_results=graph_results if isinstance(graph_results, str) else "",
return await rag_search( query=query,
query=extracted_text,
kb_id=kb_id,
top_k=BAIKE_RAG_TOP_K
)
else:
return await rag_search(
query=extracted_text,
top_k=BAIKE_RAG_TOP_K
)
async def _do_graph_search():
return await graph_rag_search.ainvoke({
"query": query,
"top_k": BAIKE_GRAPH_TOP_K
})
rag_result, graph_results = await asyncio.gather(
_do_rag_search(),
_do_graph_search()
) )
evidence_items = await _rerank_evidence(query, evidence_items)
# 处理 RAG 结果,提取成列表格式
rag_search_result = []
if rag_result.get("success", False):
source_citation = rag_result.get("sourceCitation", {})
for resource, items in source_citation.items():
for item in items:
rag_search_result.append({
"text": item.get("text", ""),
"filename": item.get("filename", ""),
"resource": item.get("resource", ""),
"score": item.get("score", 0.0)
})
rag_search_result = sorted(rag_search_result,key=lambda x:float(x.get("score") or 0), reverse=True)[:BAIKE_RESULT_LIMIT]
rag_search_result = dedupe_rag_results(rag_search_result, limit=BAIKE_RESULT_LIMIT)
return { return {
"rag_search_result": rag_search_result, "rag_search_result": rag_results,
"graph_search_result": graph_results if isinstance(graph_results, str) else "" "graph_search_result": graph_results if isinstance(graph_results, str) else "",
"evidence_items": evidence_items,
} }
except Exception as e: except Exception as exc:
return { return {
"rag_search_result": [], "rag_search_result": [],
"graph_search_result": "", "graph_search_result": "",
"error_message": f"检索失败: {str(e)}" "evidence_items": [],
"error_message": f"检索失败: {exc}",
} }
async def generate_suggested_replies_baike(answer: str, question: str) -> List[Dict[str, Any]]: async def generate_suggested_replies_baike(answer: str, question: str) -> List[Dict[str, Any]]:
""" prompt = BAIKE_PROMPTS["generate_suggested_replies"].format(question=question, answer=answer[:500])
生成针对问答结果的建议回复问题
使用大模型生成两个相关问题
"""
prompt = BAIKE_PROMPTS["generate_suggested_replies"].format(
question=question,
answer=answer[:500]
)
try: try:
result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None, json_output=True) result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None, json_output=True)
# 尝试解析JSON json_match = re.search(r"\[.*\]", result or "", re.DOTALL)
# 提取JSON部分
json_match = re.search(r'\[.*\]', result, re.DOTALL)
if json_match: if json_match:
json_str = json_match.group(0) replies = json.loads(json_match.group(0))
suggested_replies = json.loads(json_str) formatted = [
# 验证格式 {"title": str(x["title"]), "content": str(x["content"])}
if isinstance(suggested_replies, list) and len(suggested_replies) >= 2: for x in replies[:2]
# 确保每个元素都有title和content if isinstance(x, dict) and x.get("title") and x.get("content")
formatted_replies = [] ]
for reply in suggested_replies[:2]: if len(formatted) == 2:
if isinstance(reply, dict) and "title" in reply and "content" in reply: return formatted
formatted_replies.append({ except Exception:
"title": str(reply["title"]), pass
"content": str(reply["content"]) return [
}) {"title": "查看相关依据", "content": "这个回答对应的资料依据有哪些?"},
if len(formatted_replies) == 2: {"title": "继续展开说明", "content": "请把这个问题再展开说明一下。"},
return formatted_replies ]
# 如果解析失败,返回默认问题
return [
{"title": "这个设备的具体工作原理是什么?", "content": "这个设备的具体工作原理是什么?"},
{"title": "还有哪些相关的知识点?", "content": "还有哪些相关的知识点?"}
]
except Exception as e:
# 如果生成失败,返回默认问题
return [
{"title": "这个设备的具体工作原理是什么?", "content": "这个设备的具体工作原理是什么?"},
{"title": "还有哪些相关的知识点?", "content": "还有哪些相关的知识点?"}
]
@track_function_calls @track_function_calls
async def generate_answer(state: QAState) -> Dict[str, Any]: async def generate_answer(state: QAState) -> Dict[str, Any]:
""" question = state.get("combined_query") or state.get("extracted_text") or ""
正在生成问答结果 evidence_items = state.get("evidence_items") or []
"""
extracted_text = state.get("combined_query", "")
rag_results = state.get("rag_search_result", [])
graph_results = state.get("graph_search_result", "")
history_messages = state.get("history_messages", []) or []
need_retrieval = state.get("need_retrieval", True) need_retrieval = state.get("need_retrieval", True)
print("history_message(qa)", history_messages) history_messages = state.get("history_messages", []) or []
print("是否使用检索结果:", need_retrieval)
try: try:
# 1. 处理检索结果(如果有) source_text = ""
rag_ctx_parts: List[str] = [] original_image_paths: List[str] = []
file_name_list = [] if need_retrieval and evidence_items:
all_source_text = "" source_text = _format_evidence_context(evidence_items)
original_image_paths = [] original_image_paths = extract_image_paths(source_text)
source_counts = _count_sources(evidence_items)
_emit_event(
f"✨已融合 {len(evidence_items)} 条证据",
f"RAG {source_counts.get('rag', 0)} 条,图谱 {source_counts.get('graph', 0)}",
)
if need_retrieval: domain_desc = (
# 收集所有 RAG 结果文本(只取前 4 条) "舰船维修相关的法规、规范、行业标准及技术文件"
for item in sorted(rag_results or [], key=lambda x: float(x.get("score") or 0) if isinstance(x,dict) else 0, reverse=True)[:4]: if state.get("route_flag") == "rules"
# for item in (rag_results or [])[:1]: else "工业设备的结构原理、维护规程、故障诊断与维修操作"
print(item) )
print(111111111111111111111111111)
if isinstance(item, dict):
text = (item.get("text") or "").strip()
file_name = (item.get("filename") or "").strip()
if text:
rag_ctx_parts.append(text)
if file_name:
file_name_list.append(file_name)
# 处理图谱结果 if need_retrieval and source_text.strip():
print("123456", graph_results) system_prompt = BAIKE_PROMPTS["generate_answer_with_retrieval_system"].format(domain_desc=domain_desc)
if graph_results and "图谱检索完成,但未返回有效数据。" in graph_results: user_prompt = BAIKE_PROMPTS["generate_answer_with_retrieval_user"].format(
graph_results = "" extracted_text=question,
print("654321", graph_results) rag_count=len(evidence_items),
all_source_text=source_text,
# 从所有结果中提取图片路径
if graph_results:
all_source_text = graph_results + "\n" + "\n".join(rag_ctx_parts)
else:
all_source_text = "\n".join(rag_ctx_parts)
original_image_paths = extract_image_paths(all_source_text)
print("提取到的原始图片路径:", original_image_paths)
# 发送事件通知(可选)
title_options = [f"✨已综合 {len(rag_ctx_parts)} 条检索结果", f"✨本次检索策略为综合多源信息"]
start_event = {
"type": "function_execution",
"title": random.choice(title_options),
"details": f"综合了 {len(rag_ctx_parts)} 条检索结果"
}
for callback in get_all_callbacks():
try:
callback(start_event)
except Exception:
pass
query_desc = extracted_text if extracted_text else "当前未明确描述问题"
# 构建系统提示词
if state.get("route_flag") == "rules":
domain_desc = "舰船修理相关的法规、规范、行业标准及技术文件"
else:
domain_desc = "工业设备的结构原理、维护规程、故障诊断与维修操作"
# ========== 第一步:专注于生成高质量内容 ==========
# 根据是否有检索结果构建不同的提示词
if need_retrieval and all_source_text.strip():
content_system_prompt = BAIKE_PROMPTS["generate_answer_with_retrieval_system"].format(domain_desc=domain_desc)
content_user_content = BAIKE_PROMPTS["generate_answer_with_retrieval_user"].format(
extracted_text=extracted_text,
rag_count=len(rag_ctx_parts),
all_source_text=all_source_text
) )
else: else:
content_system_prompt = BAIKE_PROMPTS["generate_answer_without_retrieval_system"].format(domain_desc=domain_desc) system_prompt = BAIKE_PROMPTS["generate_answer_without_retrieval_system"].format(domain_desc=domain_desc)
content_user_content = BAIKE_PROMPTS["generate_answer_without_retrieval_user"].format(extracted_text=extracted_text) user_prompt = BAIKE_PROMPTS["generate_answer_without_retrieval_user"].format(extracted_text=question)
content_messages = [] messages = []
if history_messages: if history_messages:
content_messages.extend(history_messages) messages.extend(history_messages)
content_messages.append({"role": "user", "content": content_user_content}) messages.append({"role": "user", "content": user_prompt})
title_options = ["🤖 诊断分析中", "🤖 故障预检中"]
start_event = {
"type": "function_execution",
"title": random.choice(title_options),
"details": "正在生成回答..."
}
for callback in get_all_callbacks():
try:
callback(start_event)
except Exception:
pass
_emit_event(random.choice(["🔍 证据整理中", "🧠 正在生成回答"]), "正在基于检索资料生成回答...")
answer = await stream_generate_and_postprocess( answer = await stream_generate_and_postprocess(
system_prompt=content_system_prompt, system_prompt=system_prompt,
messages=content_messages, messages=messages,
original_image_paths=original_image_paths, original_image_paths=original_image_paths,
temperature=0.5, temperature=0.2,
fallback="抱歉,无法生成回答。" fallback="抱歉,无法生成回答。",
) )
print("llm output", answer) suggested_replies = await generate_suggested_replies_baike(answer, question)
return {"response": answer, "actions": [], "result_tag": "qa", "suggestedReplies": suggested_replies}
except Exception as exc:
return {"response": f"生成回答失败:{exc}", "actions": [], "suggestedReplies": []}
async def _search_rag_variants(queries: List[str], kb_id: Optional[str] = None) -> List[Dict[str, Any]]:
tasks = [rag_search(query=q, kb_id=kb_id, top_k=BAIKE_RAG_TOP_K) for q in queries if q.strip()]
results = await asyncio.gather(*tasks, return_exceptions=True) if tasks else []
items: List[Dict[str, Any]] = []
for result in results:
if isinstance(result, Exception) or not result.get("success"):
continue
for resource, rows in (result.get("sourceCitation") or {}).items():
for row in rows:
item = dict(row)
item.setdefault("resource", resource)
items.append(item)
items = sorted(items, key=lambda x: float(x.get("score") or 0), reverse=True)
return dedupe_rag_results(items, limit=BAIKE_RESULT_LIMIT)
def _build_evidence_items(
rag_results: List[Dict[str, Any]],
graph_results: str,
query: str,
) -> List[Dict[str, Any]]:
candidates: List[Dict[str, Any]] = []
for item in rag_results or []:
candidates.append({
"source_type": "rag",
"title": item.get("filename") or item.get("resource") or "RAG 检索结果",
"filename": item.get("filename") or item.get("resource") or "",
"text": item.get("text") or "",
"score": float(item.get("score") or 0),
"file_id": str(item.get("file_id") or ""),
"slice_id": str(item.get("id") or item.get("slice_id") or ""),
"metadata": item,
})
graph_text = str(graph_results or "").strip()
if graph_text and "图谱检索完成,但未返回有效数据。" not in graph_text and graph_text != "查询无结果":
candidates.append({
"source_type": "graph",
"title": "图谱检索结果",
"filename": "",
"text": graph_text,
"score": 0.45,
"file_id": "",
"slice_id": "",
"metadata": {},
})
deduped: Dict[str, Dict[str, Any]] = {}
for item in candidates:
text = _clean_text(item.get("text") or "")
if not text:
continue
item["text"] = text[:5000]
key = item.get("slice_id") or _content_key(text)
local_score = _local_evidence_score(query, item)
item["local_score"] = local_score
existing = deduped.get(key)
if not existing or local_score > float(existing.get("local_score") or 0):
deduped[key] = item
return sorted(deduped.values(), key=lambda x: float(x.get("local_score") or 0), reverse=True)
async def _rerank_evidence(query: str, evidence_items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
if not query.strip() or not evidence_items:
return evidence_items[:BAIKE_EVIDENCE_LIMIT]
selected = evidence_items[:24]
documents = [
"\n".join([
str(item.get("title") or ""),
str(item.get("filename") or ""),
str(item.get("text") or ""),
])[:4000]
for item in selected
]
rows = await OpenaiAPI.rerank(query=query, documents=documents, top_k=BAIKE_EVIDENCE_LIMIT)
if not rows:
return evidence_items[:BAIKE_EVIDENCE_LIMIT]
ranked: List[Dict[str, Any]] = []
seen = set()
for row in rows:
idx = int(row.get("index") or 0)
if idx < 0 or idx >= len(selected) or idx in seen:
continue
seen.add(idx)
item = dict(selected[idx])
item["rerank_score"] = float(row.get("relevance_score") or 0.0)
ranked.append(item)
for idx, item in enumerate(selected):
if idx not in seen and len(ranked) < BAIKE_EVIDENCE_LIMIT:
ranked.append(item)
return ranked[:BAIKE_EVIDENCE_LIMIT]
def _format_evidence_context(items: List[Dict[str, Any]]) -> str:
parts = []
label_map = {"rag": "RAG", "graph": "Graph"}
for idx, item in enumerate(items[:BAIKE_EVIDENCE_LIMIT], start=1):
source = label_map.get(item.get("source_type"), item.get("source_type") or "unknown")
score = item.get("rerank_score", item.get("local_score", item.get("score", "")))
meta_lines = [
f"[Evidence {idx}] source={source} score={score}",
f"Title: {item.get('title') or ''}",
]
if item.get("filename"):
meta_lines.append(f"File: {item.get('filename')}")
if item.get("file_id"):
meta_lines.append(f"File ID: {item.get('file_id')}")
if item.get("slice_id"):
meta_lines.append(f"Slice ID: {item.get('slice_id')}")
meta_lines.append("Content:")
meta_lines.append(str(item.get("text") or ""))
parts.append("\n".join(meta_lines))
return "\n\n---\n\n".join(parts)
def _local_evidence_score(query: str, item: Dict[str, Any]) -> float:
base = {"rag": 1.0, "graph": 0.65}.get(item.get("source_type"), 0.8)
raw = float(item.get("score") or 0)
score = base + min(max(raw, 0.0), 1.0)
text = " ".join([
str(item.get("title") or ""),
str(item.get("filename") or ""),
str(item.get("text") or "")[:1500],
]).lower()
title = str(item.get("title") or "").lower()
for term in _extract_search_terms(query):
t = term.lower()
if not t:
continue
if t in title:
score += 0.7
elif t in text:
score += 0.25
return score
def _build_search_queries(main_query: str, original_query: str = "", extra_queries: Optional[List[str]] = None) -> List[str]:
candidates: List[str] = []
for text in [main_query, original_query, *(extra_queries or [])]:
text = _strip_query_noise(text)
if not text:
continue
candidates.append(text)
regex_terms = _extract_search_terms(text)
if regex_terms:
candidates.append(" ".join(regex_terms[:4]))
candidates.extend(regex_terms[:4])
return _dedupe_strings(candidates)[:10]
def _extract_search_terms(query: str) -> List[str]:
stop = {"什么", "如何", "怎么", "哪些", "主要", "内容", "相关", "信息", "一下", "这个", "那个", "是否", "有没有"}
terms = []
for token in re.findall(r"[\u4e00-\u9fff]{2,12}|[A-Za-z0-9][A-Za-z0-9_\-]{1,30}", query or ""):
token = token.strip(":,。.;;、()()[]【】")
if len(token) < 2 or token in stop or re.fullmatch(r"\d+", token):
continue
terms.append(token)
return _dedupe_strings(terms)
def _strip_query_noise(text: str) -> str:
text = str(text or "").strip().strip('"“”')
if "用户问题:" in text:
text = text.split("用户问题:", 1)[1].strip()
return re.sub(r"\s+", " ", text)
def _parse_json_object(raw: str) -> Dict[str, Any]:
if not raw:
return {}
try:
return json.loads(raw)
except Exception:
match = re.search(r"\{.*\}", raw, re.DOTALL)
if match:
try:
return json.loads(match.group(0))
except Exception:
return {}
return {}
def _json_dumps(value: Any) -> str:
try:
return json.dumps(value, ensure_ascii=False)
except Exception:
return str(value)
def _content_key(text: str) -> str:
return re.sub(r"\s+", "", text or "")[:220]
def _clean_text(text: str) -> str:
return re.sub(r"\n{3,}", "\n\n", str(text or "")).strip()
def _dedupe_strings(values: List[str]) -> List[str]:
out = []
seen = set()
for value in values:
value = str(value or "").strip()
if not value or value in seen:
continue
seen.add(value)
out.append(value)
return out
def _count_sources(items: List[Dict[str, Any]]) -> Dict[str, int]:
counts: Dict[str, int] = {}
for item in items:
key = str(item.get("source_type") or "unknown")
counts[key] = counts.get(key, 0) + 1
return counts
def _emit_event(title: str, details: str) -> None:
event = {"type": "function_execution", "title": title, "details": details}
for callback in get_all_callbacks():
try:
callback(event)
except Exception:
pass
# 生成建议回复问题
suggested_replies = await generate_suggested_replies_baike(answer, extracted_text)
return {
"response": answer,
"actions": [],
"result_tag": "qa",
"suggestedReplies": suggested_replies
}
except Exception as e:
return {
"response": f"生成回答失败:{str(e)}",
"actions": [],
"suggestedReplies": []
}
def route_after_judge(state: QAState) -> str: def route_after_judge(state: QAState) -> str:
""" return "rewrite_query_with_history" if state.get("need_retrieval", True) else "generate_answer"
判断后的路由根据need_retrieval决定下一步
"""
need_retrieval = state.get("need_retrieval", True)
if need_retrieval:
return "rewrite_query_with_history"
else:
return "generate_answer"
# =============== 构建工作流 ===============
def create_baike_workflow(): def create_baike_workflow():
"""创建普通问答工作流"""
workflow = StateGraph(QAState) workflow = StateGraph(QAState)
# 添加节点
workflow.add_node("get_baike_history_context", get_baike_history_context) workflow.add_node("get_baike_history_context", get_baike_history_context)
workflow.add_node("judge_need_retrieval", judge_need_retrieval) workflow.add_node("judge_need_retrieval", judge_need_retrieval)
workflow.add_node("rewrite_query_with_history", rewrite_query_with_history) workflow.add_node("rewrite_query_with_history", rewrite_query_with_history)
workflow.add_node("search_rag_and_graph", search_rag_and_graph) workflow.add_node("search_rag_and_graph", search_rag_and_graph)
workflow.add_node("generate_answer", generate_answer) workflow.add_node("generate_answer", generate_answer)
# 设置边
workflow.add_edge(START, "get_baike_history_context") workflow.add_edge(START, "get_baike_history_context")
workflow.add_edge("get_baike_history_context", "judge_need_retrieval") workflow.add_edge("get_baike_history_context", "judge_need_retrieval")
workflow.add_conditional_edges( workflow.add_conditional_edges(
"judge_need_retrieval", "judge_need_retrieval",
route_after_judge, route_after_judge,
{ {"rewrite_query_with_history": "rewrite_query_with_history", "generate_answer": "generate_answer"},
"rewrite_query_with_history": "rewrite_query_with_history",
"generate_answer": "generate_answer"
}
) )
workflow.add_edge("rewrite_query_with_history", "search_rag_and_graph") workflow.add_edge("rewrite_query_with_history", "search_rag_and_graph")
workflow.add_edge("search_rag_and_graph", "generate_answer") workflow.add_edge("search_rag_and_graph", "generate_answer")
workflow.add_edge("generate_answer", END) workflow.add_edge("generate_answer", END)
# 编译工作流
return workflow.compile() return workflow.compile()
# =============== 工作流执行函数 ===============
async def run_baike_workflow( async def run_baike_workflow(
extracted_text: str, extracted_text: str,
combined_query: str, combined_query: str,
history_message: str = "", history_message: str = "",
route_flag: str = "" route_flag: str = "",
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""
执行普通问答工作流统一接口
Args:
extracted_text: 文本描述统一的输入参数
history_message: 历史消息目前只接收不做处理
Returns:
工作流执行结果
"""
# 创建并编译工作流
app = create_baike_workflow() app = create_baike_workflow()
# 初始化状态统一接口只使用extracted_text和history_message
initial_state = { initial_state = {
"extracted_text": extracted_text, "extracted_text": extracted_text,
"combined_query": combined_query, "combined_query": combined_query,
"retrieval_query": "", "retrieval_query": "",
"retrieval_queries": [],
"route_flag": route_flag, "route_flag": route_flag,
"history_message": history_message, "history_message": history_message,
"history_messages": [], "history_messages": [],
"need_retrieval": None, "need_retrieval": None,
"rag_search_result": None, "rag_search_result": None,
"graph_search_result": None, "graph_search_result": None,
"evidence_items": None,
"response": "", "response": "",
"actions": [], "actions": [],
"suggestedReplies": [], "suggestedReplies": [],
"error_message": "" "error_message": "",
} }
try: try:
# 执行工作流(使用异步方式)
final_state = await app.ainvoke(initial_state) final_state = await app.ainvoke(initial_state)
# 检查是否有错误
if final_state.get("error_message"): if final_state.get("error_message"):
return { return {
"response": f"普通问答工作流执行失败: {final_state['error_message']}", "response": f"普通问答工作流执行失败: {final_state['error_message']}",
"actions": [], "actions": [],
"result_tag": "qa", "result_tag": "qa",
"suggestedReplies": [] "suggestedReplies": [],
} }
# 统一输出格式:完全透传 generate_answer 的结果
return { return {
"response": final_state.get("response", ""), "response": final_state.get("response", ""),
"actions": final_state.get("actions", []), "actions": final_state.get("actions", []),
"result_tag": "qa", "result_tag": "qa",
"suggestedReplies": final_state.get("suggestedReplies", []) "suggestedReplies": final_state.get("suggestedReplies", []),
} }
except Exception as exc:
except Exception as e:
return { return {
"response": f"普通问答工作流执行失败: {str(e)}", "response": f"普通问答工作流执行失败: {exc}",
"actions": [], "actions": [],
"result_tag": "qa", "result_tag": "qa",
"suggestedReplies": [] "suggestedReplies": [],
} }
# =============== 测试 ===============
if __name__ == "__main__":
import asyncio
# 测试用例
test_cases = [
"船舰主发动机的安全警告是什么",
]
async def test():
for i, test_text in enumerate(test_cases, 1):
print(f"\n{'='*60}")
print(f"测试用例 {i}")
print(f"{'='*60}")
# ✅ 修复:传入 combined_query 参数
result = await run_baike_workflow(
extracted_text=test_text,
combined_query=test_text
)
print(f"\n执行结果:")
print(f"成功:{result.get('success', False)}")
print(f"\n响应内容:")
print(result.get("response", "无响应"))
asyncio.run(test())

View File

@ -926,12 +926,15 @@ def remove_duplicate_content(text: str) -> str:
return "\n".join(result) return "\n".join(result)
# 推荐调用顺序: # 推荐调用顺序:
# #
# text = remove_duplicate_content(text) # text = remove_duplicate_content(text)
# text = normalize_latex_spacing(text) # text = normalize_latex_spacing(text)
def _extract_rag_text(item: Any) -> str: def _extract_rag_text(item: Any) -> str:
if isinstance(item, dict): if isinstance(item, dict):
return str(item.get("text") or item.get("content") or "") return str(item.get("text") or item.get("content") or "")
@ -1316,9 +1319,13 @@ async def stream_generate_and_postprocess(
stream_handler.send_stream_content(accumulated_content) stream_handler.send_stream_content(accumulated_content)
answer = answer.strip() if answer else fallback answer = answer.strip() if answer else fallback
print("111-",answer)
answer = re.sub(r'ynchroneg>.*?ost switching>', '', answer, flags=re.DOTALL) answer = re.sub(r'ynchroneg>.*?ost switching>', '', answer, flags=re.DOTALL)
print("222-",answer)
answer = remove_duplicate_content(answer) answer = remove_duplicate_content(answer)
print("333-",answer)
answer = normalize_markdown_images(answer, original_paths=original_image_paths) answer = normalize_markdown_images(answer, original_paths=original_image_paths)
print("444-",answer)
answer = normalize_latex_spacing(answer) answer = normalize_latex_spacing(answer)
return answer.strip() if answer else fallback return answer.strip() if answer else fallback