"""图谱答案生成节点
输入: state['input'] 用户原始提问
state['cypher_query'] 执行过的语句,用来告诉模型每份结果是问什么的
state['cypher_results'] run_cypher_node 查回来的行
输出: state['neo4j_answer'] 根据查询结果组织的自然语言回答
state['output'] 同一份回答,整张图的统一出口
把 list[dict] 交给模型转写成人话,而不是在 Python 里拼字符串:返回列名由生成的 cypher
决定(herb / effect / formula ...),拼接逻辑得跟着 cypher 变,交给模型更省事。
约束模型只用查询结果作答,是这个节点的重点。图谱问答的价值就在于答案可溯源,
一旦允许模型补充自己的知识,就退化成了普通问答,还看不出哪句话是图里来的。
"""
import json
from langchain_core.messages import HumanMessage, SystemMessage
from _004_langgraph_more_nodes.agent_state import AgentState
from common.llm import my_llm
llm = my_llm
# 结果行数可能很多,全塞进 prompt 既慢又容易超长。每条语句最多给这么多行。
MAX_ROWS_PER_QUERY = 30
SYSTEM_PROMPT = (
"你是一个中医知识问答助手。下面给你的是从中医知识图谱里查询出来的结果,"
"请依据这些结果回答用户的提问。\n\n"
"【要求】\n"
"1. 只使用查询结果中的信息作答,不要补充你自己知道的中医知识,也不要推测。\n"
"2. 查询结果为空时,直接说明图谱中没有查到相关信息,不要编造。\n"
"3. 结果条目多时归类整理,用简洁的中文分点表述,不要罗列原始字段名。\n"
"4. 不要提及 cypher、数据库、查询语句这些技术细节,直接回答内容。\n"
"5. 涉及具体用药时,末尾提示需遵医嘱。"
)
EMPTY_ANSWER = "抱歉,图谱中没有查询到与您的问题相关的信息。"
def _build_result_block(state: AgentState) -> str:
"""把每条语句和它的结果配对,拼成给模型看的文本"""
cyphers = state.get("cypher_query") or []
results = state.get("cypher_results") or []
blocks = []
for i, rows in enumerate(results):
# cypher_results 和 cypher_query 下标对齐,但防一手长度不一致
cypher = cyphers[i] if i < len(cyphers) else ""
shown = rows[:MAX_ROWS_PER_QUERY]
omitted = len(rows) - len(shown)
lines = [f"【查询 {i + 1}】{cypher}"]
if not rows:
lines.append("结果:无")
else:
lines.append(f"结果(共 {len(rows)} 条):")
for row in shown:
lines.append(f" {json.dumps(row, ensure_ascii=False)}")
if omitted > 0:
lines.append(f" ...另有 {omitted} 条已省略")
blocks.append("\n".join(lines))
return "\n\n".join(blocks)
def generate_neo4j_answer_node(state: AgentState) -> AgentState:
print("图谱答案节点执行开始了")
user_input = (state.get("input_semantic_trans") or "").strip()
results = state.get("cypher_results") or []
# 所有语句都没查回东西,不必调用大模型 —— 模型能说的也只是"没查到"
if not user_input or not any(results):
state["neo4j_answer"] = EMPTY_ANSWER
state["output"] = EMPTY_ANSWER
print("完成生成图谱答案: 无结果")
return state
messages = [
SystemMessage(content=SYSTEM_PROMPT),
HumanMessage(
content=(
f"用户提问:{user_input}\n\n"
f"图谱查询结果:\n{_build_result_block(state)}"
)
),
]
answer = llm.invoke(messages).content.strip()
state["neo4j_answer"] = answer
state["output"] = answer
history_messages = state.get("history_messages", [])
history_messages.append({"role": "assistant", "content": answer})
state["history_messages"] = history_messages
print(f"完成生成图谱答案: {len(answer)} 字")
return state
if __name__ == "__main__":
cases = [
{
"input": "四君子汤由哪些药材组成?",
"cypher_query": [
"MATCH (f:Formula)-[:HAS_INGREDIENT]->(h:Herb) "
"WHERE f.name = '四君子汤' RETURN h.name AS herb LIMIT 20"
],
"cypher_results": [
[{"herb": "甘草"}, {"herb": "人参"}, {"herb": "茯苓"}, {"herb": "白术"}]
],
},
{
"input": "人参有什么功效?",
"cypher_query": [
"MATCH (h:Herb)-[:HAS_EFFECT]->(e:Effect) WHERE h.name IN ['人参'] "
"RETURN h.name AS herb, e.name AS effect LIMIT 20"
],
"cypher_results": [
[
{"herb": "人参", "effect": "安神"},
{"herb": "人参", "effect": "益气温阳"},
{"herb": "人参", "effect": "补脾健肺"},
{"herb": "人参", "effect": "大补元气"},
]
],
},
# 查询结果全空:预期不调用大模型,直接给兜底话术
{
"input": "阿司匹林是什么药材?",
"cypher_query": ["MATCH (h:Herb) WHERE h.name = '阿司匹林' RETURN h.name AS herb"],
"cypher_results": [[]],
},
]
for case in cases:
result = generate_neo4j_answer_node(dict(case))
print(f"\n提问: {case['input']}")
print(f" {result['neo4j_answer'][:200]}")
print(f" output 一致: {result['output'] == result['neo4j_answer']}")