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
+154
View File
@@ -0,0 +1,154 @@
"""信息核实节点(作业 5,LLM 决策版)。
设计:提示词即程序——核实链的顺序、话术、阈值、全局中断全部
在 ``prompts/single_confirm.md`` / ``dual_confirm.md`` 中(与工作流
chatNode 提示词一致),代码不维护状态链。
节点职责(薄运行时):
1. 把上下文注入提示词:已核实字段(person_fields,模型据此跳过
已确认项)、当前人员序号(person_index,双车两方隔离)、
对话历史、无回复计数;
2. 一次 LLM 调用产出 ``<state>`` 前缀回复;前缀缺失/非法提议
受控修复一次(复用 collect 的语义),仍失败抛 CollectReplyError;
3. 字段提交走**独立提取调用**(计划作业 4-4 允许,需记录次数):
从本轮输入提取语义明确的新字段,validate_candidate 校验后
按 person_index 隔离写入 person_fields / turn_patch;
4. 阶段码一律经白名单校验(3001/3002 → 0000/0001/0002/0003/0004)。
模型决定“问什么、去哪一步”;代码决定“能不能去、字段能不能写”。
"""
import json
from .collect import (
_bookkeeping,
context_prompt,
conversation_history,
validated_reply,
)
from .fields import FieldCandidate, FieldStatus, validate_candidate
from .llm import LLMService
from .prompts import DUAL_CONFIRM, SINGLE_CONFIRM, render_prompt
from .state import AgentState
# 提取调用的系统提示标记(测试与分流依据;不以 <state> 开头,
# 故不会被误当对话回复解析)
_EXTRACTION_MARKER = "【字段提取】"
_EXTRACTION_PROMPT = (
_EXTRACTION_MARKER
+ "从【用户输入】中提取本次新提供且语义明确的当事人信息,"
"只输出 JSON 对象(键限于给定字段表,值用原文;无新字段输出 {{}})。"
"模糊、破碎、待确认的输入不提取。reserved 键 person_index(1 或 2)"
"仅在对话明确切换到另一方当事人时给出。\n"
"字段表(当前人员):{keys}\n"
"【用户输入】{user_input}\n"
"【模型回复】{reply}"
)
# 各方人员字段(来源:endpoints.get_info 分组;与 fields.KNOWN_FIELD_KEYS 对齐)
_PERSON1_KEYS = ("xm1", "sfzmhm1", "sfzmwh1", "sjhm1", "sjwh1", "csbw1")
_PERSON2_KEYS = ("xm2", "sfzmhm2", "sfzmwh2", "sjhm2", "sjwh2", "csbw2")
def _person_keys(person_index: int) -> tuple[str, ...]:
return _PERSON1_KEYS if person_index != 2 else _PERSON2_KEYS
async def _extract_turn_fields(
state: AgentState, llm: LLMService, reply: str
) -> tuple[list[FieldCandidate], int | None]:
"""独立提取调用:返回 (候选列表, 模型声明的 person_index 或 None)。
调用次数由调用方计入(计划作业 4-4:记录原因/次数/延迟)。
"""
person_index = state.get("person_index") or 1
raw = await llm.complete(
_EXTRACTION_PROMPT.format(
keys="、".join(_person_keys(person_index)),
user_input=state.get("input_text", ""),
reply=reply,
),
"",
)
try:
data = json.loads(raw.strip())
if not isinstance(data, dict):
return [], None
except json.JSONDecodeError:
return [], None
next_person = data.pop("person_index", None)
if next_person not in (1, 2):
next_person = None
candidates = [
FieldCandidate(key=key, value=value, source_turn=state.get("turn_count") or 0)
for key, value in data.items()
if key in _person_keys(person_index)
]
return candidates, next_person
async def _run_confirm(state: AgentState, llm: LLMService, prompt_name: str) -> dict:
"""核实节点共享运行时(单车/双车仅提示词不同)。"""
system_prompt = render_prompt(prompt_name, state) + context_prompt(state)
user_message = state.get("input_text", "")
current = state.get("stage_code") or "3001"
raw, stage = await validated_reply(
llm, system_prompt, user_message, current, history=conversation_history(state)
)
# 字段提取与按人员隔离提交(提取调用本身可能失败,不阻塞对话)
person_fields = dict(state.get("person_fields") or {})
confirm_status = dict(state.get("confirm_status") or {})
patch: dict = {}
new_person_index = state.get("person_index") or 1
try:
candidates, declared = await _extract_turn_fields(state, llm, raw)
except Exception:
candidates, declared = [], None
for cand in candidates:
validated = validate_candidate(cand)
confirm_status[validated.key] = validated.status
if validated.status is FieldStatus.CONFIRMED:
person_fields[validated.key] = validated.value
patch[validated.key] = validated.value
if declared is not None:
new_person_index = declared
no_reply_count, _clarify = _bookkeeping(state)
# clarify_count 的“计入”是模型按提示词(连续 4 次)判断的语义,
# 代码侧只做簿记透传,不用 collect 的近似增量
clarify_count = state.get("clarify_count") or 0
dialogue = list(state.get("dialogue") or [])
dialogue.append({"role": "user", "content": state.get("input_text", "")})
dialogue.append({"role": "assistant", "content": raw})
return {
"turn_count": (state.get("turn_count") or 0) + 1,
"no_reply_count": no_reply_count,
"clarify_count": clarify_count,
"response_text": raw,
"stage_code": stage,
"raw_status_code": stage,
"person_fields": person_fields,
"confirm_status": confirm_status,
"turn_patch": patch,
"person_index": new_person_index,
"dialogue": dialogue,
}
async def run_single_confirm(state: AgentState, llm: LLMService) -> dict:
"""单车信息确认节点(提示词:prompts/single_confirm.md)。"""
return await _run_confirm(state, llm, SINGLE_CONFIRM)
async def run_dual_confirm(state: AgentState, llm: LLMService) -> dict:
"""双车信息确认节点(提示词:prompts/dual_confirm.md)。
与单车共用运行时;两方的区分靠注入的 person_fields 与
person_index,提取调用的字段表按当前人员隔离,模型声明切换
到另一方(person_index)时由代码应用。
"""
return await _run_confirm(state, llm, DUAL_CONFIRM)
+92 -160
View File
@@ -1,19 +1,19 @@
"""LangGraph 事故流程图。
架构(对照 ``workflow/20261016/事故信息采集2025118.json``,作业 3 修订):
架构(对照 ``workflow/20261020/事故信息采集2025118.json``):
- 每个主要阶段是独立节点,与工作流 chatNode 一一对应:
``collect``(事故信息收集)、``single_photo``(单车拍照)、
``dual_photo``(双车拍照)、``single_confirm`` / ``dual_confirm``
(信息确认,作业 5 实现);
- 节点间的大切换由 ``route_by_stage`` 条件边按 ``stage_code`` 路由
(对照工作流的 Condition#2/#7/#9 等);LLM 接入后由节点内的
“解析状态”代码写出 ``stage_code``,条件边只做分发,不含业务规则;
- 节点内部的阶段推进(拍照步骤、失败/无回复处理、分流)由节点内
LLM 提议目标 ``stage_code``(脚手架期由 ``_fake_llm_propose``
模拟),代码按 ``transitions`` 白名单校验、非法提议不写状态;
代码只负责事件解析与计数等上下文簿记,不代替模型决定;
- LLM 接入前(作业 4 之前),节点共用固定话术模板。
- 每个主要阶段一个节点,与工作流 chatNode 一一对应:
``collect``(事故信息收集)、``single_photo`` / ``dual_photo``
(拍照)、``single_confirm`` / ``dual_confirm``(信息确认);
- 节点间大切换由 ``route_by_stage`` 条件边按 ``stage_code`` 分发
(对照 Condition#2/#7/#9 等);一轮 HTTP 请求 = 一次图执行,
跨轮记忆由 checkpoint 承载;
- 节点内阶段码一律由 LLM 提议(提示词即程序),代码只校验
(前缀格式、白名单、clientMode 出口)与落库;
- 依赖注入走 LangGraph 原生机制:``context_schema=GraphDeps``,
节点签名 ``(state, runtime: Runtime[GraphDeps])``,调用时
``context=GraphDeps(llm=...)``。LLM client 属于运行时依赖,
不进 state、不进 checkpoint(计划作业 3-2)。
图结构::
@@ -21,40 +21,43 @@
├→ collect ───────────→ END
├→ single_photo ──────→ END
├→ dual_photo ────────→ END
├→ single_confirm ────→ END (作业 5)
├→ dual_confirm ──────→ END (作业 5)
├→ single_confirm ────→ END
├→ dual_confirm ──────→ END
└→ END (终态 0000—0005)
首轮语义(对照工作流 work_start 的 is_first_message → Assigned Reply#3):
新会话第一条消息由 ``welcome`` 节点以固定话术返回 ``1001``
(未准备好通话),不调用模型;之后用户说【继续办理】或继续对话,
由 collect 节点的 LLM 提议 ``1001 → 1002``(白名单已含该转换)。
离线运行示例:
离线运行示例(默认注入 workflow fake LLM):
python -m examples.langgraph_smoke
对应测试:test/agent/test_scaffold.py、test/agent/test_events.py、
test/agent/test_transitions.py
test/agent/test_transitions.py、test/agent/test_photo.py、
test/agent/test_confirm.py
"""
from dataclasses import dataclass
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.runtime import Runtime
from .events import ClientEvent, parse_client_event
from .collect import run_collect
from .confirm import run_dual_confirm, run_single_confirm
from .events import parse_client_event
from .llm import LLMService, workflow_fake_llm
from .photo import run_photo
from .state import AgentState
from .transitions import TERMINAL_STAGES, is_legal_transition
from .transitions import TERMINAL_STAGES
# 缺省阶段(首轮 welcome 之后的兜底);后续由规则/模型驱动。
DEFAULT_STAGE_CODE = "1002"
# 首轮固定问候(对照工作流 Assigned Reply#3 + Variable Update#6):
# 新会话首条消息返回 1001,不调用模型。
WELCOME_STAGE = "1001"
WELCOME_TEXT = (
"<state>1001</state>您好,请问您是来办理交通事故信息采集的吗?"
"请说“继续办理”开始。"
)
@dataclass
class GraphDeps:
"""图节点的运行时依赖。
通过 ``context_schema`` 注入(``ainvoke(..., context=GraphDeps(...))``),
只存在于调用期,不序列化、不进入 checkpoint。
"""
llm: LLMService
def normalize_input(state: AgentState) -> dict:
@@ -99,157 +102,79 @@ def route_by_stage(state: AgentState) -> str:
return "collect"
def welcome(state: AgentState) -> dict:
"""首轮问候节点:新会话第一条消息的固定话术(对照工作流
work_start is_first_message → Assigned Reply#3)。
# 首轮固定问候(对照工作流 Assigned Reply#3):新会话首条消息返回
# 1001,不调用模型;话术与 Variable Update#6 后的记录一致。
WELCOME_STAGE = "1001"
WELCOME_TEXT = (
"<state>1001</state>您好,这里是无锡交警,我将为您远程处理交通事故。"
"请将人员撤离至路侧安全区域,开启危险报警双闪灯、放置三角警告牌、"
"做好安全防护,谨防二次事故伤害。若您已经准备好了,请点击继续办理,"
"如需人工服务,请说转人工。"
)
确定性节点,不调用模型;返回 1001(未准备好通话),
用户确认后由 collect 节点的 LLM 经白名单守卫进入 1002。
def welcome(state: AgentState) -> dict:
"""首轮问候节点(确定性,不调用模型)。
返回 1001(未准备好通话),并把问候写入 dialogue,
供后续节点作为聊天历史注入(对齐 FastGPT 的 chatId 历史)。
"""
return {
"turn_count": (state.get("turn_count") or 0) + 1,
"response_text": WELCOME_TEXT,
"stage_code": WELCOME_STAGE,
"raw_status_code": WELCOME_STAGE,
"dialogue": [
{"role": "user", "content": state.get("input_text", "")},
{"role": "assistant", "content": WELCOME_TEXT},
],
}
def _fake_llm_propose(state: AgentState, event: ClientEvent | None) -> str | None:
"""假 LLM:提出下一步阶段码(作业 4 起由真实 LLM 取代)。
模拟工作流 chatNode 提示词的判断(来源见各分支注释):
- 会话标记且当前 1001 → 1002(work_start is_first_message);
- 【拍摄完成】拍照链前进一步(单车/双车拍照提示词);
- 拍照失败标记 → 0005(客户端已统计三次,提示词要求直接转人工);
- 连续第二次【用户无回复】 → 0004(no_reply_count 由代码簿记,
本函数读取的是本轮计数前的历史值)。
提议是否生效由 ``is_legal_transition`` 校验。
"""
current = state.get("stage_code") or ""
if event in (ClientEvent.SESSION_START, ClientEvent.CONTINUE):
if current == "1001":
return "1002"
if event is ClientEvent.PHOTO_DONE:
chain = {
"2000": "2001", "2001": "2002", "2002": "2003",
"2003": "2005",
"2010": "2011", "2011": "2012", "2012": "2013",
"2013": "2014", "2014": "2015",
}
return chain.get(current)
if event is ClientEvent.PHOTO_FAILED_3X:
return "0005"
if event is ClientEvent.NO_REPLY and (state.get("no_reply_count") or 0) >= 1:
return "0004"
return None
async def collect_node(state: AgentState, runtime: Runtime[GraphDeps]) -> dict:
"""事故信息收集节点(工作流「事故信息收集」chatNode)。"""
return await run_collect(state, runtime.context.llm)
def _event_reply(state: AgentState) -> dict:
"""事件簿记 + 阶段提议/校验 + 固定话术(作业 3 脚手架版)。
- 代码只维护上下文:``no_reply_count`` 累计(NO_REPLY)与
重置(其他输入);
- 阶段码(含失败/无回复处理)一律由 ``_fake_llm_propose`` 提议
(作业 4 换真实 LLM),经白名单校验,非法提议保持当前阶段;
- 轮数 +1,响应带 ``<state>`` 前缀(API 层解析)。
TODO(作业 4): 真实 LLM 接入后,话术与阶段提议移入各节点;
TODO(作业 4-10): 对非法提议做一次受控修复重试(当前直接拒绝)。
"""
raw_event = state.get("client_event") or ""
event = ClientEvent(raw_event) if raw_event else None
no_reply_count = state.get("no_reply_count") or 0
if event is ClientEvent.NO_REPLY:
no_reply_count += 1
elif (state.get("input_text") or "").strip():
no_reply_count = 0
current = state.get("stage_code") or DEFAULT_STAGE_CODE
proposed = _fake_llm_propose(state, event)
if proposed and is_legal_transition(current, proposed):
stage_code = proposed
else:
stage_code = current
response_text = (
f"<state>{stage_code}</state>"
f"已收到:{state['input_text']}"
)
return {
"turn_count": (state.get("turn_count") or 0) + 1,
"no_reply_count": no_reply_count,
"response_text": response_text,
"stage_code": stage_code,
}
async def single_photo_node(state: AgentState, runtime: Runtime[GraphDeps]) -> dict:
"""单车拍照节点(工作流「单车拍照」/「单车拍照#6」chatNode)。"""
return await run_photo(state, runtime.context.llm, kind="single")
def collect(state: AgentState) -> dict:
"""事故信息收集节点(工作流“事故信息收集” chatNode)。
TODO(作业 4): 接入 LLM——多轮采集、跳过已知信息、一次补齐分流;
模型回复中的状态前缀由代码校验(解析状态,对照工作流
“解析状态” code 节点),不由模型自由写阶段。
"""
return _event_reply(state)
async def dual_photo_node(state: AgentState, runtime: Runtime[GraphDeps]) -> dict:
"""双车拍照节点(工作流「双车拍照」/「双车拍照#7」chatNode)。"""
return await run_photo(state, runtime.context.llm, kind="dual")
def single_photo(state: AgentState) -> dict:
"""单车拍照节点(工作流“单车拍照” chatNode)。
TODO(作业 5): 按 2000→2001→2002→2003→2005 生成对应拍照话术;
步骤推进的确定性部分已在 ``transitions`` 表覆盖。
"""
return _event_reply(state)
async def single_confirm_node(state: AgentState, runtime: Runtime[GraphDeps]) -> dict:
"""单车信息确认节点(工作流「单车信息确认」chatNode)。"""
return await run_single_confirm(state, runtime.context.llm)
def dual_photo(state: AgentState) -> dict:
"""双车拍照节点(工作流“双车拍照” chatNode)。
TODO(作业 5): 按 2010→…→2015 生成对应话术;direct 模式末次
拍摄完成进入 3002(同轮询问第一方身份)。
"""
return _event_reply(state)
async def dual_confirm_node(state: AgentState, runtime: Runtime[GraphDeps]) -> dict:
"""双车信息确认节点(工作流「双车信息确认」chatNode)。"""
return await run_dual_confirm(state, runtime.context.llm)
def single_confirm(state: AgentState) -> dict:
"""单车信息确认节点(工作流“单车信息确认” chatNode,作业 5)。"""
raise NotImplementedError("TODO(作业 5): 实现单车信息确认节点")
def dual_confirm(state: AgentState) -> dict:
"""双车信息确认节点(工作流“双车信息确认” chatNode,作业 5)。"""
raise NotImplementedError("TODO(作业 5): 实现双车信息确认节点")
def build_graph(checkpointer=None, llm=None):
def build_graph(checkpointer=None):
"""装配并编译图。
参数:
checkpointer: 自定义 checkpointer;缺省 ``InMemorySaver``,
作业 7 替换为 ``AsyncPostgresSaver``。
llm: LLM 服务(``llm.LLMService``);提供后 collect 节点
走 ``collect.run_collect``(作业 4),否则保持离线
脚手架行为(``_event_reply``,作业 1—3 的测试依赖)。
调用方通过 ``config={"configurable": {"thread_id": <会话>}}`` 区分会话。
LLM 依赖不在此处绑定——调用时通过
``config={"configurable": {"thread_id": <会话>}}`` 区分会话,
``context=GraphDeps(llm=<LLMService>)`` 注入运行时依赖。
"""
builder = StateGraph(AgentState)
builder = StateGraph(AgentState, context_schema=GraphDeps)
builder.add_node("normalize_input", normalize_input)
builder.add_node("welcome", welcome)
if llm is None:
collect_node = collect
else:
from .collect import run_collect
async def collect_node(state): # noqa: F811
return await run_collect(state, llm)
builder.add_node("collect", collect_node)
builder.add_node("single_photo", single_photo)
builder.add_node("dual_photo", dual_photo)
builder.add_node("single_confirm", single_confirm)
builder.add_node("dual_confirm", dual_confirm)
builder.add_node("single_photo", single_photo_node)
builder.add_node("dual_photo", dual_photo_node)
builder.add_node("single_confirm", single_confirm_node)
builder.add_node("dual_confirm", dual_confirm_node)
builder.add_edge(START, "normalize_input")
builder.add_conditional_edges(
@@ -265,10 +190,17 @@ def build_graph(checkpointer=None, llm=None):
END: END,
},
)
builder.add_edge("welcome", END)
builder.add_edge("collect", END)
builder.add_edge("single_photo", END)
builder.add_edge("dual_photo", END)
builder.add_edge("single_confirm", END)
builder.add_edge("dual_confirm", END)
for name in (
"welcome", "collect", "single_photo", "dual_photo",
"single_confirm", "dual_confirm",
):
builder.add_edge(name, END)
return builder.compile(checkpointer=checkpointer or InMemorySaver())
__all__ = [
"AgentState",
"GraphDeps",
"build_graph",
"workflow_fake_llm",
]
+83
View File
@@ -0,0 +1,83 @@
"""拍照节点:按工作流分支选提示词,由 LLM 同时决定下一阶段和话术。
来源:workflow/20261020/事故信息采集2025118.json 的四个拍照 chatNode。
【拍摄完成】、拍照失败、无回复均作为模型输入;代码校验前缀、转换
白名单和 clientMode 出口,不覆盖模型的判断或正文。
"""
from .collect import (
_bookkeeping,
_build_user_message,
context_prompt,
conversation_history,
validated_reply,
)
from .llm import LLMService
from .prompts import (
DUAL_PHOTO,
DUAL_PHOTO_ADDON,
SINGLE_PHOTO,
SINGLE_PHOTO_ADDON,
render_prompt,
)
from .state import AgentState
from .transitions import is_legal_transition
PHOTO_PROMPTS = {
("single", "direct"): SINGLE_PHOTO,
("single", "browser_addon"): SINGLE_PHOTO_ADDON,
("dual", "direct"): DUAL_PHOTO,
("dual", "browser_addon"): DUAL_PHOTO_ADDON,
}
def is_legal_photo_transition(source: str, target: str, *, mode: str) -> bool:
"""按工作流 Condition#11/#12 校验模式出口;不替模型改写目标。
direct 拍照结束进入 3001/3002;addon 拍照结束进入 0000。
两种模式均允许提示词定义的转人工码和保持当前阶段。
"""
if not is_legal_transition(source, target):
return False
if mode == "browser_addon" and target in {"3001", "3002"}:
return False
if mode == "direct" and target == "0000":
return False
return True
async def run_photo(state: AgentState, llm: LLMService, *, kind: str) -> dict:
"""调用对应拍照模型一次;输出不合规时最多修复一次。
历史包含上一轮已经提出的拍照/身份问题,随 checkpoint 传到核实
节点,避免切换节点后丢失上下文。车损位置等自然语言保留原样。
"""
mode = state.get("client_mode") or "direct"
prompt_name = PHOTO_PROMPTS[(kind, mode)]
current = state["stage_code"]
raw, stage = await validated_reply(
llm,
render_prompt(prompt_name, state) + context_prompt(state),
_build_user_message(state),
current,
history=conversation_history(state),
validator=lambda source, target: is_legal_photo_transition(
source, target, mode=mode
),
)
no_reply_count, clarify_count = _bookkeeping(state)
dialogue = list(state.get("dialogue") or [])
dialogue.extend([
{"role": "user", "content": state.get("input_text", "")},
{"role": "assistant", "content": raw},
])
return {
"turn_count": (state.get("turn_count") or 0) + 1,
"response_text": raw,
"stage_code": stage,
"raw_status_code": stage,
"no_reply_count": no_reply_count,
"clarify_count": clarify_count,
"dialogue": dialogue,
"turn_patch": {},
}
+12 -4
View File
@@ -14,18 +14,21 @@
from collections.abc import AsyncIterator
from ..agent.graph import build_graph
from ..agent.graph import GraphDeps, build_graph, workflow_fake_llm
from .chat import ChatInput, ChatResult, ChatStreamEvent, ChatBackend, TextDelta
class LangGraphBackend(ChatBackend):
"""Adapt a compiled LangGraph to the neutral chat contract."""
def __init__(self, graph=None):
def __init__(self, graph=None, llm=None):
"""参数:
graph: 编译后的图;缺省调用 ``build_graph()``(内存 checkpoint)。
graph: 编译后的图;缺省调用 ``build_graph()``。
llm: LLM 服务(运行时依赖);缺省为离线 workflow fake。
生产环境由 lifespan 注入 ``build_llm_service()`` 的实例。
"""
self._graph = graph if graph is not None else build_graph()
self._llm = llm if llm is not None else workflow_fake_llm()
async def complete(self, chat_input: ChatInput) -> ChatResult:
"""非流式:执行一轮图并翻译结果。
@@ -40,8 +43,13 @@ class LangGraphBackend(ChatBackend):
(作业 6 起实现)。
"""
result = await self._graph.ainvoke(
{"session_id": chat_input.session_id, "input_text": chat_input.text},
{
"session_id": chat_input.session_id,
"input_text": chat_input.text,
"client_mode": chat_input.client_mode,
},
config={"configurable": {"thread_id": chat_input.session_id}},
context=GraphDeps(llm=self._llm),
)
return ChatResult(
content = result["response_text"],
+2 -2
View File
@@ -26,8 +26,8 @@ async def lifespan(app: FastAPI):
from ..agent.llm import build_llm_service
global _langgraph_graph
_langgraph_graph = build_graph(llm=build_llm_service())
_chat_backend = LangGraphBackend(_langgraph_graph)
_langgraph_graph = build_graph()
_chat_backend = LangGraphBackend(_langgraph_graph, llm=build_llm_service())
else:
_fastgpt_client = AsyncChatClient(
api_key=Config.FASTGPT_API_KEY,