test_graph.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. """qna 引擎单测:用 fake LLM 返回固定 JSON,验证图节点输出"""
  2. import json
  3. import sys
  4. from pathlib import Path
  5. sys.path.insert(0, str(Path(__file__).resolve().parents[2])) # cfc-langgraph 根
  6. from src.qna import schemas, graph, prompts # noqa: E402
  7. class FakeLLM:
  8. """返回固定决策 JSON 的假 LLM"""
  9. def __init__(self, decisions):
  10. self.decisions = list(decisions)
  11. self.calls = []
  12. def invoke(self, messages):
  13. self.calls.append(messages)
  14. d = self.decisions.pop(0) if len(self.decisions) > 1 else self.decisions[0]
  15. return type("R", (), {"content": json.dumps(d, ensure_ascii=False)})()
  16. def make_scene(**kw):
  17. base = {
  18. "scene_key": "microbiome",
  19. "opening_prompt": "了解您的肠道健康状况",
  20. "dimensions_json": {"user": ["肠道状态"], "need": ["营养需求"]},
  21. "kb_scope": ["microbiome"],
  22. "max_questions": 12,
  23. }
  24. base.update(kw)
  25. return base
  26. def test_decide_next_ask():
  27. llm = FakeLLM([{"action": "ask", "question": {
  28. "id": "q1", "type": "single", "text": "您多久吃一次蔬菜?",
  29. "options": [{"id": "a", "label": "每天"}]}, "reason": "了解饮食"}])
  30. state = graph.decide_next(llm, make_scene(), [], prompt_fn=prompts.build_decide_prompt)
  31. assert state["action"] == "ask"
  32. assert state["question"]["text"].startswith("您多久")
  33. def test_decide_next_force_finish_when_max_reached():
  34. llm = FakeLLM([{"action": "ask", "question": {"id": "q9", "type": "text", "text": "x"}}])
  35. history = [{"question": {"text": f"q{i}"}, "answer": "a"} for i in range(12)]
  36. state = graph.decide_next(llm, make_scene(max_questions=12), history, prompt_fn=prompts.build_decide_prompt)
  37. assert state["action"] == "finish"
  38. def test_decide_next_invalid_json_retries_once():
  39. llm = FakeLLM(["not json", {"action": "finish", "reason": "信息足够"}])
  40. state = graph.decide_next(llm, make_scene(), [], prompt_fn=prompts.build_decide_prompt)
  41. assert state["action"] == "finish"
  42. assert len(llm.calls) == 2 # 重试了一次
  43. def test_generate_profile_structure():
  44. llm = FakeLLM([{"user_profile": [{"dimension": "肠道状态", "score": 70, "description": "偏健康",
  45. "evidence": ["答1"]}],
  46. "need_profile": [{"dimension": "营养需求", "description": "补纤维",
  47. "evidence": ["答1"], "suggestion": "多吃粗粮"}]}])
  48. history = [{"question": {"text": "q1"}, "answer": "a"}]
  49. profile, kb_used = graph.generate_profile(llm, make_scene(), history, kb_context=[{"content": "菌属知识"}],
  50. prompt_fn=prompts.build_profile_prompt)
  51. assert "user_profile" in profile and "need_profile" in profile
  52. assert profile["user_profile"][0]["dimension"] == "肠道状态"
  53. assert 0 <= profile["user_profile"][0]["score"] <= 100
  54. assert kb_used is True