refactor(agent): inject deps via context_schema, LLM-driven photo/confirm nodes

- graph.py: LangGraph-native DI - StateGraph(context_schema=GraphDeps),
  nodes take (state, runtime: Runtime[GraphDeps]); build_graph() decoupled
  from LLM; deleted scaffold _event_reply/_fake_llm_propose/stub nodes
- photo.py: model proposes stage+body via per-mode prompts (direct/addon);
  validator enforces clientMode exits; deterministic event forcing removed
- confirm.py: single runtime for single/dual confirm, extraction call
  isolated per person_index
- llm.py: workflow_fake_llm() (offline workflow-faithful responder,
  history-aware) for smoke/tests
- backend: LangGraphBackend(graph, llm) passes context per call
- tests: invoke helpers inject GraphDeps; illegal-proposal test asserts
  error + state untouched via second well-behaved fake

Verified: 143 passed; smoke turns 1/2/1 with original welcome text
This commit is contained in:
Eric Wang
2026-10-21 15:11:16 +08:00
parent a5407e03b2
commit 6d0a2169c8
12 changed files with 794 additions and 187 deletions
+9 -4
View File
@@ -26,7 +26,8 @@ from src.agent.prompts import COLLECTION, load_prompt
def collect_state(**overrides) -> dict:
state = {
"session_id": "s1",
"input_text": "你好",
# 使用完整句而非短促词,避免触发分流守卫(“你好”<=2 字)
"input_text": "发生了交通事故,两辆车追尾",
"stage_code": "1002",
"turn_count": 1,
"no_reply_count": 0,
@@ -138,13 +139,16 @@ async def test_collect_illegal_stage_proposal_not_committed():
@pytest.mark.asyncio
async def test_graph_collect_node_with_llm():
# 图级:build_graph(llm=...) 时首轮走 welcome(不调模型),
# 第二轮起 collect 节点走 run_collect
# 图级:首轮走 welcome(确定性,不调模型);第二轮起 collect
# 节点经 Runtime 注入的 llm 走 run_collect
from src.agent.graph import GraphDeps
llm = FakeLLMService(lambda s, u: "<state>1002</state>收到。")
graph = build_graph(llm=llm)
graph = build_graph()
first = await graph.ainvoke(
{"session_id": "s1", "input_text": "追尾了"},
config={"configurable": {"thread_id": "s1"}},
context=GraphDeps(llm=llm),
)
assert len(llm.calls) == 0 # welcome 是确定性节点
assert first["stage_code"] == "1001"
@@ -152,6 +156,7 @@ async def test_graph_collect_node_with_llm():
result = await graph.ainvoke(
{"session_id": "s1", "input_text": "继续办理"},
config={"configurable": {"thread_id": "s1"}},
context=GraphDeps(llm=llm),
)
assert len(llm.calls) == 1
assert result["response_text"].startswith("<state>1002</state>")