""" 工作流:普通问答Agent(使用LangGraph) 输入:文本(转化后和原文本query)、图片描述、语音文本 步骤: 1. 使用RAG和GraphRAG检索相关信息 2. 生成回答 使用 OpenaiAPI 方法调用大模型,尽可能使用异步 """ 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 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_current_stream_handler, get_all_callbacks from utils.image_utils import extract_image_paths, find_closest_image_path, normalize_markdown_images, get_image_prompt_guidance from prompts import BAIKE_PROMPTS, COMMON_PROMPTS import asyncio import json import re import random # =============== 定义状态 =============== class QAState(TypedDict): """普通问答工作流状态""" extracted_text: str # 已提取的文本(统一接口,只接收文本) combined_query: str retrieval_query: str # 用于RAG/图谱检索的重写后query route_flag: str # 路由标识 history_message: str # 历史消息(JSON格式) history_messages: List[Dict[str, Any]] # 过滤后的历史消息列表 need_retrieval: Optional[bool] # 是否需要检索 # 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 # 错误信息 # =============== 辅助函数 =============== import re def normalize_latex_spacing(text: str) -> str: """ 规范 Markdown 中的 LaTeX 公式。 处理: - $...$:行内公式,清理内侧空格并补充正文间空格 - $$...$$:段落公式,完整保留开始和结束定界符 - \\(...\\):转换为 $...$ - \\[...\\]:转换为 $$...$$ 不修改: - Markdown 围栏代码块 - Markdown 行内代码 - HTML code/pre/script/style - HTML 注释 - 转义美元符号 \\$ """ if not isinstance(text, str): raise TypeError( f"text 必须是 str,实际类型为 {type(text).__name__}" ) if not text: return text text = text.replace("\r\n", "\n").replace("\r", "\n") protected_parts: list[str] = [] def protect(content: str) -> str: token = ( f"\uE000LATEX_PROTECTED_" f"{len(protected_parts)}" f"\uE001" ) protected_parts.append(content) return token def restore(content: str) -> str: for index, original in enumerate(protected_parts): token = f"\uE000LATEX_PROTECTED_{index}\uE001" content = content.replace(token, original) return content def is_escaped(content: str, position: int) -> bool: slash_count = 0 position -= 1 while position >= 0 and content[position] == "\\": slash_count += 1 position -= 1 return slash_count % 2 == 1 # ================================================================ # 保护 Markdown 围栏代码块 # ================================================================ lines = text.splitlines(keepends=True) output_lines: list[str] = [] line_index = 0 while line_index < len(lines): line = lines[line_index] fence_match = re.match( r"^[ \t]{0,3}(`{3,}|~{3,})", line, ) if not fence_match: output_lines.append(line) line_index += 1 continue opening_fence = fence_match.group(1) fence_character = opening_fence[0] fence_length = len(opening_fence) closing_pattern = re.compile( rf"^[ \t]{{0,3}}" rf"{re.escape(fence_character)}" rf"{{{fence_length},}}" rf"[ \t]*(?:\n|$)" ) block_lines = [line] line_index += 1 while line_index < len(lines): current_line = lines[line_index] block_lines.append(current_line) line_index += 1 if closing_pattern.match(current_line): break output_lines.append( protect("".join(block_lines)) ) text = "".join(output_lines) # ================================================================ # 保护 HTML 代码区域和注释 # ================================================================ text = re.sub( r"(?is)" r"<(?Ppre|code|script|style)\b[^>]*>" r".*?" r"", lambda match: protect(match.group(0)), text, ) text = re.sub( r"(?s)", lambda match: protect(match.group(0)), text, ) # ================================================================ # 保护 Markdown 链接地址 # ================================================================ text = re.sub( r"(!?\[[^\]\n]*\]\()([^)\n]*)(\))", lambda match: ( match.group(1) + protect(match.group(2)) + match.group(3) ), text, ) # ================================================================ # 保护 Markdown 行内代码 # ================================================================ inline_code_output: list[str] = [] position = 0 while position < len(text): if text[position] != "`": inline_code_output.append(text[position]) position += 1 continue delimiter_end = position while ( delimiter_end < len(text) and text[delimiter_end] == "`" ): delimiter_end += 1 delimiter = text[position:delimiter_end] search_position = delimiter_end closing_position = -1 while search_position < len(text): candidate = text.find( delimiter, search_position, ) if candidate < 0: break previous_character = ( text[candidate - 1] if candidate > 0 else "" ) next_position = candidate + len(delimiter) next_character = ( text[next_position] if next_position < len(text) else "" ) if ( previous_character != "`" and next_character != "`" ): closing_position = candidate break search_position = candidate + 1 if closing_position < 0: inline_code_output.append(delimiter) position = delimiter_end continue end_position = closing_position + len(delimiter) inline_code_output.append( protect(text[position:end_position]) ) position = end_position text = "".join(inline_code_output) # ================================================================ # 转换其他 LaTeX 定界符 # ================================================================ # \( x + y \) -> $x + y$ text = re.sub( r"(? $$x + y$$ text = re.sub( r"(?s)(? 0 and text[position - 1] == "$" ) ): opening_position = position search_position = position + 1 closing_position = -1 while search_position < len(text): candidate = text.find( "$", search_position, ) if candidate < 0: break # 行内公式不允许跨行。 if "\n" in text[ opening_position + 1:candidate ]: break if is_escaped(text, candidate): search_position = candidate + 1 continue if text.startswith("$$", candidate): search_position = candidate + 2 continue next_character = ( text[candidate + 1] if candidate + 1 < len(text) else "" ) # 避免把 $100 和 $200 配对成公式。 if next_character.isdigit(): search_position = candidate + 1 continue closing_position = candidate break if closing_position < 0: result.append("$") position += 1 continue formula_body = text[ opening_position + 1:closing_position ].strip() if not formula_body: result.append( text[ opening_position: closing_position + 1 ] ) position = closing_position + 1 continue formula_body = re.sub( r"[ \t]+", " ", formula_body, ) previous_character = "" if result and result[-1]: previous_character = result[-1][-1] next_character = ( text[closing_position + 1] if closing_position + 1 < len(text) else "" ) if ( previous_character and not previous_character.isspace() and previous_character not in opening_punctuation ): result.append(" ") result.append( "$" + formula_body + "$" ) if ( next_character and not next_character.isspace() and next_character not in closing_punctuation ): result.append(" ") position = closing_position + 1 continue result.append(text[position]) position += 1 return restore("".join(result)) def remove_duplicate_content(text: str) -> str: """ 删除普通 Markdown 正文中的完全重复行。 不对以下内容进行去重: - $$ ... $$ 段落公式 - \\[ ... \\] 段落公式 - equation、align、gather 等 LaTeX 环境 - Markdown 围栏代码块 - Markdown 缩进代码块 - 标题、列表、引用、表格等 Markdown 结构行 这样不会删除段落公式末尾的第二个 $$。 """ if not text or len(text) < 10: return text text = text.replace("\r\n", "\n").replace("\r", "\n") lines = text.split("\n") result: list[str] = [] seen_segments: set[str] = set() in_code_block = False code_fence_character = "" code_fence_length = 0 in_dollar_math = False in_bracket_math = False latex_environment_stack: list[str] = [] display_environments = { "equation", "equation*", "align", "align*", "alignat", "alignat*", "gather", "gather*", "multline", "multline*", "flalign", "flalign*", "eqnarray", "eqnarray*", "displaymath", "math", } def is_escaped(value: str, position: int) -> bool: slash_count = 0 position -= 1 while position >= 0 and value[position] == "\\": slash_count += 1 position -= 1 return slash_count % 2 == 1 def count_unescaped_double_dollars(value: str) -> int: count = 0 position = 0 while position < len(value) - 1: if ( value[position:position + 2] == "$$" and not is_escaped(value, position) ): count += 1 position += 2 else: position += 1 return count def is_markdown_structure_line(value: str) -> bool: stripped = value.strip() if not stripped: return True patterns = ( # Markdown 标题 r"^#{1,6}\s+", # 无序列表 r"^[-+*]\s+", # 有序列表 r"^\d+[.)]\s+", # 引用 r"^>\s*", # 任务列表 r"^[-+*]\s+\[[ xX]\]\s+", # 水平分隔线 r"^(?:-{3,}|\*{3,}|_{3,})$", # Markdown 表格分隔行 r"^\|?\s*:?-{3,}:?" r"(?:\s*\|\s*:?-{3,}:?)+\s*\|?$", # 普通表格行 r"^\|.*\|$", # HTML 标签 r"^]*>$", # LaTeX 环境边界 r"^\\(?:begin|end)\{[^}]+\}", # 公式定界符 r"^(?:\$\$|\\\[|\\\])$", # Markdown 链接引用定义 r"^\[[^\]]+\]:\s*\S+", ) return any( re.match(pattern, stripped) for pattern in patterns ) for line in lines: line = line.rstrip() stripped = line.strip() # ============================================================ # Markdown 围栏代码块 # ============================================================ fence_match = re.match( r"^[ \t]{0,3}(`{3,}|~{3,})", line, ) if not in_code_block and fence_match: fence = fence_match.group(1) in_code_block = True code_fence_character = fence[0] code_fence_length = len(fence) result.append(line) continue if in_code_block: result.append(line) closing_pattern = re.compile( rf"^[ \t]{{0,3}}" rf"{re.escape(code_fence_character)}" rf"{{{code_fence_length},}}" rf"[ \t]*$" ) if closing_pattern.match(line): in_code_block = False code_fence_character = "" code_fence_length = 0 continue # Markdown 缩进代码块。 if re.match(r"^(?:\t| {4})", line): result.append(line) continue # ============================================================ # \[ ... \] 段落公式 # ============================================================ if in_bracket_math: result.append(line) if re.search(r"(? bracket_close_count: in_bracket_math = True continue # ============================================================ # LaTeX display 环境 # ============================================================ begin_matches = re.findall( r"\\begin\{([^}]+)\}", line, ) end_matches = re.findall( r"\\end\{([^}]+)\}", line, ) relevant_begins = [ environment for environment in begin_matches if environment in display_environments ] relevant_ends = [ environment for environment in end_matches if environment in display_environments ] if latex_environment_stack: result.append(line) for environment in relevant_begins: latex_environment_stack.append(environment) for environment in relevant_ends: if ( latex_environment_stack and latex_environment_stack[-1] == environment ): latex_environment_stack.pop() elif environment in latex_environment_stack: latex_environment_stack.remove(environment) continue if relevant_begins: result.append(line) for environment in relevant_begins: latex_environment_stack.append(environment) for environment in relevant_ends: if ( latex_environment_stack and latex_environment_stack[-1] == environment ): latex_environment_stack.pop() elif environment in latex_environment_stack: latex_environment_stack.remove(environment) continue # ============================================================ # $$ ... $$ 段落公式 # ============================================================ double_dollar_count = ( count_unescaped_double_dollars(line) ) if in_dollar_math: # 公式内部和结束的 $$ 全部原样保留。 result.append(line) if double_dollar_count % 2 == 1: in_dollar_math = False continue if double_dollar_count > 0: # 包含 $$ 的行不参与去重。 result.append(line) # 奇数个 $$ 表示开启了跨行公式。 if double_dollar_count % 2 == 1: in_dollar_math = True continue # ============================================================ # 空行和 Markdown 结构行 # ============================================================ if not stripped: result.append(line) continue if is_markdown_structure_line(line): result.append(line) continue # ============================================================ # 仅对普通正文行去重 # ============================================================ normalized_line = re.sub( r"\s+", " ", stripped.lower(), ) if normalized_line in seen_segments: continue seen_segments.add(normalized_line) result.append(line) return "\n".join(result) # 推荐调用顺序: # # text = remove_duplicate_content(text) # text = normalize_latex_spacing(text) # =============== 节点函数 =============== def get_baike_history_context(state: QAState) -> Dict[str, Any]: """ 知识问答步骤:获取问答类历史信息 问答工作流只关注历史问答记录,用于提供上下文 """ history_message = state.get("history_message", "") or "" return init_history(history_message) async def judge_need_retrieval(state: QAState) -> Dict[str, Any]: """ 判断用户问题是否需要检索知识库 """ original_query = (state.get("combined_query") or state.get("extracted_text") or "").strip() history_messages = state.get("history_messages", []) or [] prompt = BAIKE_PROMPTS["judge_need_retrieval"].format( history_messages=history_messages if history_messages else '无', original_query=original_query ) need_retrieval = True try: result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None) if result: result = result.strip().lower() if result == "false": need_retrieval = False except Exception as e: print(f"判断是否需要检索失败: {e}") need_retrieval = True print(f"是否需要检索: {need_retrieval}") return {"need_retrieval": need_retrieval} 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() history_messages = state.get("history_messages", []) or [] # 控制历史长度:只保留最近的若干条,避免上下文过长 trimmed_history = history_messages[-8:] if history_messages else [] 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 = "" if trimmed_history: # 从后往前找最近一条 role == "user" 的消息 for msg in reversed(trimmed_history): if msg.get("role") == "user": last_user_query = msg.get("content", "").strip() break prompt = BAIKE_PROMPTS["rewrite_query_with_history"].format( last_user_query=last_user_query or '(无)', history_str=history_str, original_query=original_query ) rewritten_query = original_query try: result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None) if result: candidate = result.strip().strip("“”\"'") # 只在模型返回的内容明显为单行短文本时替换,避免异常输出 if candidate and len(candidate) <= 200 and "\n" not in candidate: rewritten_query = candidate except Exception as e: print(f"query 重写失败: {e}") title_options = [f"✨正在分析中,请稍等..."] 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 print("原始检索query:", original_query) print("重写后检索query:", last_user_query+rewritten_query) return {"retrieval_query":last_user_query+rewritten_query} @track_function_calls async def search_rag_and_graph(state: QAState) -> Dict[str, Any]: """ 进行文本知识检索和图谱知识检索 """ kb_id = None 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: # 异步并行调用RAG检索和图谱检索 print(kb_id) print(33333333333333333333333333333333333333) async def _do_rag_search(): if kb_id: return await rag_search( query=extracted_text, kb_id=kb_id, top_k=2 ) else: return await rag_search( query=extracted_text, top_k=2 ) async def _do_graph_search(): return await graph_rag_search.ainvoke({ "query": query, "top_k": 3 }) rag_result, graph_results = await asyncio.gather( _do_rag_search(), _do_graph_search() ) # 处理 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) }) # 只保留前 4 条数据 # rag_search_result = rag_search_result[:2] rag_search_result = sorted(rag_search_result,key=lambda x:float(x.get("socre") or 0), reverse=True)[:4] return { "rag_search_result": rag_search_result, "graph_search_result": graph_results if isinstance(graph_results, str) else "" } except Exception as e: return { "rag_search_result": [], "graph_search_result": "", "error_message": f"检索失败: {str(e)}" } 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] ) try: result = await OpenaiAPI.open_api_chat_without_thinking(prompt, model=None, json_output=True) # 尝试解析JSON # 提取JSON部分 json_match = re.search(r'\[.*\]', result, re.DOTALL) if json_match: json_str = json_match.group(0) suggested_replies = json.loads(json_str) # 验证格式 if isinstance(suggested_replies, list) and len(suggested_replies) >= 2: # 确保每个元素都有title和content formatted_replies = [] for reply in suggested_replies[:2]: if isinstance(reply, dict) and "title" in reply and "content" in reply: formatted_replies.append({ "title": str(reply["title"]), "content": str(reply["content"]) }) if len(formatted_replies) == 2: return formatted_replies # 如果解析失败,返回默认问题 return [ {"title": "这个设备的具体工作原理是什么?", "content": "这个设备的具体工作原理是什么?"}, {"title": "还有哪些相关的知识点?", "content": "还有哪些相关的知识点?"} ] except Exception as e: # 如果生成失败,返回默认问题 return [ {"title": "这个设备的具体工作原理是什么?", "content": "这个设备的具体工作原理是什么?"}, {"title": "还有哪些相关的知识点?", "content": "还有哪些相关的知识点?"} ] @track_function_calls async def generate_answer(state: QAState) -> Dict[str, Any]: """ 正在生成问答结果 """ 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) print("history_message(qa)", history_messages) print("是否使用检索结果:", need_retrieval) try: # 1. 处理检索结果(如果有) rag_ctx_parts: List[str] = [] file_name_list = [] all_source_text = "" original_image_paths = [] if need_retrieval: # 收集所有 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]: # for item in (rag_results or [])[:1]: 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) # 处理图谱结果 print("123456", graph_results) if graph_results and "图谱检索完成,但未返回有效数据。" in graph_results: graph_results = "" print("654321", graph_results) # 从所有结果中提取图片路径 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: content_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) content_messages = [] if history_messages: content_messages.extend(history_messages) content_messages.append({"role": "user", "content": content_user_content}) # 第一步:生成内容 raw_content = await OpenaiAPI.open_api_chat_without_thinking( model=None, system_prompt=content_system_prompt, messages=content_messages, temperature=0.5 ) # 过滤掉异常标签(如果有) raw_content = re.sub(r'ynchroneg>.*?ost switching>', '', raw_content, flags=re.DOTALL) raw_content = raw_content.strip() if raw_content else "抱歉,无法生成回答。" title_options = ["🤖 诊断分析中", "🤖 故障预检中"] start_event = { "type": "function_execution", "title": random.choice(title_options), "details": raw_content[:30] + "..." } for callback in get_all_callbacks(): try: callback(start_event) except Exception: pass # ========== 第二步:专注于规范格式 ========== # 构建第二步的提示词:只关注格式规范,不修改内容 format_system_prompt = COMMON_PROMPTS["format_system"] format_user_content = COMMON_PROMPTS["format_user_with_image_examples"].format(raw_content=raw_content) # 第二步:规范格式(流式输出) stream_handler = get_current_stream_handler() answer = "" accumulated_content = "" # 用于逐步发送给前端的内容 # 使用 async for 遍历流式生成器 async for chunk in OpenaiAPI.open_api_chat_stream( model=None, system_prompt=format_system_prompt, messages=[{"role": "user", "content": format_user_content}], temperature=0.1 ): answer += chunk accumulated_content += chunk # 每收到新的 chunk 就发送一次更新 if stream_handler: stream_handler.send_stream_content(accumulated_content) answer = answer.strip() if answer else raw_content # LaTeX 公式空格规范化 # def normalize_latex_spacing(text: str) -> str: # placeholder = "\x00DOUBLE_DOLLAR\x00" # text = text.replace("$$", placeholder) # text = re.sub(r'(? str: """ 判断后的路由:根据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(): """创建普通问答工作流""" workflow = StateGraph(QAState) # 添加节点 workflow.add_node("get_baike_history_context", get_baike_history_context) workflow.add_node("judge_need_retrieval", judge_need_retrieval) 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("generate_answer", generate_answer) # 设置边 workflow.add_edge(START, "get_baike_history_context") workflow.add_edge("get_baike_history_context", "judge_need_retrieval") workflow.add_conditional_edges( "judge_need_retrieval", route_after_judge, { "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("search_rag_and_graph", "generate_answer") workflow.add_edge("generate_answer", END) # 编译工作流 return workflow.compile() # =============== 工作流执行函数 =============== async def run_baike_workflow( extracted_text: str, combined_query: str, history_message: str = "", route_flag: str = "" ) -> Dict[str, Any]: """ 执行普通问答工作流(统一接口) Args: extracted_text: 文本描述(统一的输入参数) history_message: 历史消息(目前只接收,不做处理) Returns: 工作流执行结果 """ # 创建并编译工作流 app = create_baike_workflow() # 初始化状态(统一接口,只使用extracted_text和history_message) initial_state = { "extracted_text": extracted_text, "combined_query": combined_query, "retrieval_query": "", "route_flag": route_flag, "history_message": history_message, "history_messages": [], "need_retrieval": None, "rag_search_result": None, "graph_search_result": None, "response": "", "actions": [], "suggestedReplies": [], "error_message": "" } try: # 执行工作流(使用异步方式) final_state = await app.ainvoke(initial_state) # 检查是否有错误 if final_state.get("error_message"): return { "response": f"普通问答工作流执行失败: {final_state['error_message']}", "actions": [], "result_tag": "qa", "suggestedReplies": [] } # 统一输出格式:完全透传 generate_answer 的结果 return { "response": final_state.get("response", ""), "actions": final_state.get("actions", []), "result_tag": "qa", "suggestedReplies": final_state.get("suggestedReplies", []) } except Exception as e: return { "response": f"普通问答工作流执行失败: {str(e)}", "actions": [], "result_tag": "qa", "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())