bugfix:修复部分提示词导致的格式错误问题及langfuse客户端复用bug

This commit is contained in:
2026-09-27 23:23:34 +08:00
parent 3ce1c1ae61
commit e5efaf507d
9 changed files with 117 additions and 35 deletions
+63 -19
View File
@@ -6,19 +6,19 @@ from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.graph import END, StateGraph
from app.processor.nodes.action_node import ActionNode, can_generate_actions
from app.processor.nodes.clarify_node import ClarifyNode
from app.processor.nodes.component_resource_node import ComponentResourceNode
from app.processor.nodes.fallback_node import FallbackNode
from app.processor.nodes.input_node import InputNode
from app.processor.nodes.intent_node import IntentNode
from app.processor.nodes.requirement_node import RequirementNode
from app.processor.nodes.wait_for_user_node import WaitForUserNode
from app.processor.nodes.qa_node import QANode
from app.processor.nodes.scope_node import ScopeNode
from app.processor.nodes.action_node import ActionNode, can_generate_actions
from app.processor.nodes.validator_node import ValidatorNode
from app.processor.nodes.reflection_node import ReflectionNode
from app.processor.nodes.fallback_node import FallbackNode
from app.processor.nodes.planner_node import PlannerNode
from app.processor.nodes.component_resource_node import ComponentResourceNode
from app.processor.nodes.qa_node import QANode
from app.processor.nodes.reflection_node import ReflectionNode
from app.processor.nodes.requirement_node import RequirementNode
from app.processor.nodes.scope_node import ScopeNode
from app.processor.nodes.validator_node import ValidatorNode
from app.processor.nodes.wait_for_user_node import WaitForUserNode
from app.processor.state import QueryState
@@ -207,6 +207,7 @@ query_app = build_graph()
if __name__ == "__main__":
import argparse
import asyncio
import json
from uuid import uuid4
@@ -217,11 +218,25 @@ if __name__ == "__main__":
from app.clients.decision import close_decision_engine
from app.observability.tracing import ainvoke_screen, flush_tracing
# 选择场景时只切换用户需求,共用同一份画布,方便比较执行结果
examples = {
"move": "把销售额统计向右移动 50px,其他内容保持不变",
"layout": (
"重新排列现有两个图表,销售额统计放在左侧,销售占比放在右侧,"
"统一尺寸为 600×400,画布改为 1600×900,不新增或删除组件"
),
"add": "保留现有组件,在下方新增一个柱状图,使用物料示例数据,不需要真实数据绑定",
"clarify": "这个图不太对",
}
parser = argparse.ArgumentParser(description="运行大屏 Agent 完整流程示例")
parser.add_argument("--case", choices=examples, default="move", help="选择联调场景")
args = parser.parse_args()
async def main():
# 1. 修改请求,测试完整流程
# 1. 创建请求,每次运行使用独立会话,避免混入之前的澄清历史
state: QueryState = {
"request": {
"prompt": "帮我修改销售额统计这个图,但具体改哪里我还没说,请先问我",
"prompt": examples[args.case],
"screen": {
"canvas": {"width": 1920, "height": 1080},
"nodes": [
@@ -231,7 +246,19 @@ if __name__ == "__main__":
"name": "销售额统计",
"layout": {"x": 26, "y": 34.8, "width": 400, "height": 260},
"props": {"option": {"title": {"text": "销售额统计"}}},
}
},
{
"id": "sales-share",
"type": "pie-chart",
"name": "销售占比",
"layout": {
"x": 500,
"y": 34.8,
"width": 400,
"height": 260,
},
"props": {"option": {"title": {"text": "销售占比"}}},
},
],
"dataSource": [],
},
@@ -245,10 +272,15 @@ if __name__ == "__main__":
state["request"].get("context", {}).get("conversation_id") or thread_id
)
print(f"场景:{args.case}\n需求:{examples[args.case]}")
if args.case == "clarify":
print("澄清回答示例:把销售额统计向右移动 50px,其他内容保持不变")
try:
final_state = await ainvoke_screen(
query_app, state, thread_id=thread_id, session_id=session_id
)
# 3. 展示真实澄清问题,用同一个 thread_id 恢复流程
while final_state.get("__interrupt__"):
pending = final_state["__interrupt__"][0].value
print(pending["question"])
@@ -277,15 +309,24 @@ if __name__ == "__main__":
await close_decision_engine()
flush_tracing()
# 3. 打印完整 state
# 4. 聚焦最终结果,校验失败时 simulated_screen 应为 None
result = {
"status": final_state.get("status"),
"intent": final_state.get("intent"),
# 保留需求与规划,方便定位模型输出和执行能力之间的不一致
"requirement": final_state.get("requirement"),
"screen_plan": final_state.get("screen_plan"),
"validation_passed": final_state.get("validation_passed"),
"retry_count": final_state.get("retry_count", 0),
"failure_reason": final_state.get("failure_reason"),
"validation_errors": final_state.get("validation_errors", []),
"actions": final_state.get("actions", []),
"simulated_screen": final_state.get("simulated_screen"),
"answer": final_state.get("answer"),
}
print(
json.dumps(
{
key: value.model_dump(mode="json", exclude_unset=True)
if isinstance(value, BaseModel)
else value
for key, value in final_state.items()
},
result,
ensure_ascii=False,
indent=2,
default=lambda value: (
@@ -295,5 +336,8 @@ if __name__ == "__main__":
),
)
)
# 原始坐标单独展示,确认后端试执行没有修改请求中的画布
original = state["request"]["screen"]["nodes"][0]["layout"]
print(f"原始销售额坐标:{original['x']},{original['y']}")
asyncio.run(main())
+33 -6
View File
@@ -2,7 +2,9 @@
import json
from langchain_core.exceptions import OutputParserException
from langchain_core.messages import HumanMessage, SystemMessage
from pydantic import ValidationError
from app.clients.llm import get_llm
from app.processor.base import BaseNode
@@ -28,14 +30,39 @@ class ClarifyNode(BaseNode):
payload["requirement"] = state["requirement"].model_dump(mode="json")
llm_client = get_llm(Clarification)
clarification = await llm_client.ainvoke(
[
SystemMessage(content=CLARIFY_SYSTEM_PROMPT),
HumanMessage(content=json.dumps(payload, ensure_ascii=False)),
]
)
messages = [
SystemMessage(content=CLARIFY_SYSTEM_PROMPT),
HumanMessage(content=json.dumps(payload, ensure_ascii=False)),
]
# 格式错误重试一次
for attempt in range(2):
try:
clarification = await llm_client.ainvoke(messages)
break
except (ValidationError, OutputParserException):
if attempt == 0:
# 要求重新生成完整问题,避免截断选项改变原意
messages.append(
HumanMessage(
content=(
"上次输出未通过 Clarification 格式校验,请重新生成完整结果:"
"question 为非空问题,options 为空或含 2–3 项,"
"每项包含唯一 id、非空 label 和 description,最多一个 recommended=true"
)
)
)
continue
# 连续格式失败时退回自由回答,需求阶段沿用已确定的问题
requirement = state.get("requirement") if intent != "unclear" else None
question = (
requirement.clarification_question if requirement else None
) or "你希望我对当前大屏做什么?可以描述想修改的内容,或想了解的问题"
clarification = Clarification(question=question)
updated_state["clarification"] = clarification
# 在进入 interrupt 前保存等待状态,暂停中的节点尚不会返回更新
updated_state["status"] = "waiting_for_input"
return updated_state
+2
View File
@@ -68,6 +68,8 @@ class InputNode(BaseNode):
updated_state["retry_count"] = 0
updated_state["repair_plan"] = None
updated_state["failure_reason"] = None
# 首次请求进入处理状态,恢复澄清时也使用同一状态
updated_state["status"] = "running"
return updated_state
+2
View File
@@ -28,6 +28,8 @@ class QANode(BaseNode):
if not answer:
raise ValueError("问答模型未返回文本回答")
updated_state["answer"] = answer
# 回答生成完成,问答分支在此结束
updated_state["status"] = "completed"
return updated_state
+4
View File
@@ -15,6 +15,8 @@ class ValidatorNode(BaseNode):
def process(self, state: QueryState) -> QueryState:
updated_state: QueryState = state.copy()
# 校验失败仍可能进入修正循环,最终失败由 Fallback 标记
updated_state["status"] = "running"
actions = state["actions"]
try:
# 重新校验模型内容,避免赋值后绕过字段校验
@@ -130,6 +132,8 @@ class ValidatorNode(BaseNode):
updated_state["validation_errors"] = []
# 保存整批动作执行后的 JSON,供后续检查,不代表前端已经完成更新
updated_state["simulated_screen"] = screen
# 仅表示后端动作已通过校验,前端执行结果需另行反馈
updated_state["status"] = "completed"
return updated_state
+2 -2
View File
@@ -49,5 +49,5 @@ class QueryState(TypedDict):
] # 待合并的用户回答或取消指令
answer: NotRequired[str | None] # 问答节点生成的回答或建议
status: NotRequired[
Literal["running", "cancelled", "failed"]
] # 流程继续、用户取消或动作修正失败
Literal["running", "waiting_for_input", "completed", "cancelled", "failed"]
] # 后端处理、等待补充、处理完成、用户取消或失败,完成不代表前端已执行