feat: 添加结构化日志系统,更新LLM配置与全部文档
新增: - backend/logger.py — 集中日志模块 (JSON格式 + trace_id + 独立llm.log) - @log_node / @_log_route 装饰器覆盖17个节点和8个路由 改进: - backend/llm.py — _LLMLoggingWrapper 自动记录LLM输入输出 - backend/llm.py — API Key优先读ANTHROPIC_API_KEY,模型名改为MiniMax-M2.7 - backend/llm.py — get_llm() 新增caller参数标识调用来源 - backend/validation.py — 新增验证结果/连接失败日志 - backend/session.py — 新增会话创建/删除日志 - app.py — 新增用户交互日志 (输入/执行/异常/会话操作) - app.py — 提前导入torchvision抑制transformers懒加载报错 - .env.example — 新增LOG_DIR/LOG_LEVEL/ANTHROPIC_API_KEY等配置项 - .gitignore — 新增logs/和db/忽略规则 文档: - ROADMAP.md — 新增阶段四: 可观测性 - README.md — 补充日志架构/LLM配置/项目结构 - CLAUDE.md — 同步最新配置/日志/MAX_RETRY(3) - CODE_GUIDE.md — 新增第15章日志系统,更新架构图/LLM/配置
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""LangGraph JRXML 生成代理的状态图定义。"""
|
||||
|
||||
import functools
|
||||
import os
|
||||
from typing import Literal
|
||||
|
||||
@@ -25,14 +26,41 @@ from agent.nodes import (
|
||||
correct_jrxml,
|
||||
finalize,
|
||||
)
|
||||
from backend.logger import get_logger
|
||||
|
||||
load_dotenv()
|
||||
MAX_RETRY = int(os.getenv("MAX_RETRY", "3"))
|
||||
|
||||
_graph_log = get_logger("agent")
|
||||
|
||||
|
||||
def _log_route(route_name: str):
|
||||
"""装饰器:自动记录路由决策。"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(state: AgentState, *args, **kwargs):
|
||||
target = func(state, *args, **kwargs)
|
||||
_graph_log.info(
|
||||
f"[路由] {route_name} → {target}",
|
||||
extra={
|
||||
"route": route_name,
|
||||
"target": target,
|
||||
"session_id": state.get("session_id", ""),
|
||||
"intent": state.get("intent", ""),
|
||||
"status": state.get("status", ""),
|
||||
"has_jrxml": bool(state.get("current_jrxml", "").strip()),
|
||||
"retry_count": state.get("retry_count", 0),
|
||||
},
|
||||
)
|
||||
return target
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
# ============================================================
|
||||
# 路由函数
|
||||
# ============================================================
|
||||
|
||||
@_log_route("route_by_intent")
|
||||
def route_by_intent(state: AgentState) -> Literal[
|
||||
"retrieve", "modify_jrxml", "save_session",
|
||||
"handle_consult", "handle_undo", "handle_reset"
|
||||
@@ -59,18 +87,22 @@ def route_by_intent(state: AgentState) -> Literal[
|
||||
return "retrieve"
|
||||
|
||||
|
||||
@_log_route("route_after_generate")
|
||||
def route_after_generate(state: AgentState) -> Literal["save_session"]:
|
||||
return "save_session"
|
||||
|
||||
|
||||
@_log_route("route_after_modify")
|
||||
def route_after_modify(state: AgentState) -> Literal["save_session"]:
|
||||
return "save_session"
|
||||
|
||||
|
||||
@_log_route("route_after_undo")
|
||||
def route_after_undo(state: AgentState) -> Literal["save_session"]:
|
||||
return "save_session"
|
||||
|
||||
|
||||
@_log_route("route_after_save")
|
||||
def route_after_save(state: AgentState) -> Literal["validate", "finalize"]:
|
||||
# 预览/导出意图跳过验证,直接完成
|
||||
intent = state.get("intent", "")
|
||||
@@ -79,16 +111,19 @@ def route_after_save(state: AgentState) -> Literal["validate", "finalize"]:
|
||||
return "validate"
|
||||
|
||||
|
||||
@_log_route("route_after_validate")
|
||||
def route_after_validate(state: AgentState) -> Literal["finalize", "explain_error"]:
|
||||
if state.get("status") == "pass":
|
||||
return "finalize"
|
||||
return "explain_error"
|
||||
|
||||
|
||||
@_log_route("route_after_explain")
|
||||
def route_after_explain(state: AgentState) -> Literal["correct_jrxml"]:
|
||||
return "correct_jrxml"
|
||||
|
||||
|
||||
@_log_route("route_after_correct")
|
||||
def route_after_correct(state: AgentState) -> Literal["validate", "finalize"]:
|
||||
retry = state.get("retry_count", 0)
|
||||
if retry >= MAX_RETRY:
|
||||
|
||||
+86
-7
@@ -1,9 +1,11 @@
|
||||
"""LangGraph JRXML 生成工作流的节点函数。"""
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict
|
||||
|
||||
@@ -11,21 +13,82 @@ from dotenv import load_dotenv
|
||||
|
||||
from agent.state import AgentState
|
||||
from backend.llm import get_llm
|
||||
from backend.logger import get_logger, set_trace_id
|
||||
from backend.validation import validate_jrxml
|
||||
from prompts.loader import load_prompt
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_node_log = get_logger("agent")
|
||||
|
||||
MAX_RETRY = int(os.getenv("MAX_RETRY", "3"))
|
||||
CONTEXT_MAX_TOKENS = int(os.getenv("CONTEXT_MAX_TOKENS", "6000"))
|
||||
CONTEXT_KEEP_RECENT = int(os.getenv("CONTEXT_KEEP_RECENT", "4"))
|
||||
HISTORY_MAX_SNAPSHOTS = int(os.getenv("HISTORY_MAX_SNAPSHOTS", "10"))
|
||||
|
||||
|
||||
def _state_summary(state: AgentState) -> dict:
|
||||
"""提取 state 中的关键字段用于日志摘要。"""
|
||||
user_input = state.get("user_input", "")
|
||||
return {
|
||||
"session_id": state.get("session_id", ""),
|
||||
"intent": state.get("intent", ""),
|
||||
"status": state.get("status", ""),
|
||||
"has_jrxml": bool(state.get("current_jrxml", "").strip()),
|
||||
"jrxml_length": len(state.get("current_jrxml", "")),
|
||||
"retry_count": state.get("retry_count", 0),
|
||||
"user_input_preview": user_input[:100] if user_input else "",
|
||||
"conversation_turns": len(state.get("conversation_history", [])),
|
||||
"history_snapshots": len(state.get("history_states", [])),
|
||||
"versions": len(state.get("jrxml_versions", [])),
|
||||
}
|
||||
|
||||
|
||||
def log_node(node_name: str):
|
||||
"""装饰器:自动记录节点入口、出口和耗时。"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(state: AgentState, *args, **kwargs):
|
||||
t0 = time.time()
|
||||
_node_log.info(
|
||||
f"[节点入口] {node_name}",
|
||||
extra={"node": node_name, "phase": "entry", "state": _state_summary(state)},
|
||||
)
|
||||
try:
|
||||
result = func(state, *args, **kwargs)
|
||||
elapsed_ms = round((time.time() - t0) * 1000)
|
||||
_node_log.info(
|
||||
f"[节点出口] {node_name}",
|
||||
extra={
|
||||
"node": node_name,
|
||||
"phase": "exit",
|
||||
"duration_ms": elapsed_ms,
|
||||
"state": _state_summary(state),
|
||||
},
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
elapsed_ms = round((time.time() - t0) * 1000)
|
||||
_node_log.error(
|
||||
f"[节点异常] {node_name}: {e}",
|
||||
extra={
|
||||
"node": node_name,
|
||||
"phase": "error",
|
||||
"duration_ms": elapsed_ms,
|
||||
"error": str(e),
|
||||
"state": _state_summary(state),
|
||||
},
|
||||
)
|
||||
raise
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 核心工作流节点
|
||||
# ============================================================
|
||||
|
||||
@log_node("process_input")
|
||||
def process_input(state: AgentState) -> Dict:
|
||||
"""记录用户输入到对话历史,重置本轮请求状态。如有上次失败上下文则自动注入。"""
|
||||
user_input = state.get("user_input", "")
|
||||
@@ -58,6 +121,7 @@ def process_input(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("save_state_snapshot")
|
||||
def save_state_snapshot(state: AgentState) -> Dict:
|
||||
"""保存当前状态快照到 history_states,用于撤销操作。最多保留 N 个版本。"""
|
||||
snapshots = state.get("history_states", [])
|
||||
@@ -82,6 +146,7 @@ def save_state_snapshot(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("classify_intent")
|
||||
def classify_intent(state: AgentState) -> Dict:
|
||||
"""使用 LLM 对用户输入进行意图分类(8 种意图)。"""
|
||||
user_input = state.get("user_input", "")
|
||||
@@ -89,7 +154,7 @@ def classify_intent(state: AgentState) -> Dict:
|
||||
|
||||
intent = "initial_generation"
|
||||
try:
|
||||
llm = get_llm()
|
||||
llm = get_llm(caller="classify_intent")
|
||||
prompt = load_prompt("intent_classify").format(
|
||||
has_report=has_report,
|
||||
user_input=user_input[:500],
|
||||
@@ -116,11 +181,12 @@ def classify_intent(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("handle_consult")
|
||||
def handle_consult(state: AgentState) -> Dict:
|
||||
"""处理咨询类问题:调用 LLM 直接回答,不走报表生成流程。"""
|
||||
user_input = state.get("user_input", "")
|
||||
try:
|
||||
llm = get_llm()
|
||||
llm = get_llm(caller="handle_consult")
|
||||
prompt = load_prompt("consult").format(question=user_input)
|
||||
resp = llm.invoke(prompt)
|
||||
answer = resp.content.strip()
|
||||
@@ -135,6 +201,7 @@ def handle_consult(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("handle_undo")
|
||||
def handle_undo(state: AgentState) -> Dict:
|
||||
"""撤销上一步修改:从 history_states 恢复最近一个快照。"""
|
||||
snapshots = state.get("history_states", [])
|
||||
@@ -162,6 +229,7 @@ def handle_undo(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("handle_reset")
|
||||
def handle_reset(state: AgentState) -> Dict:
|
||||
"""重置当前会话:清空报表相关状态,保留会话信息。"""
|
||||
state["current_jrxml"] = ""
|
||||
@@ -186,6 +254,7 @@ def handle_reset(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("count_tokens")
|
||||
def count_tokens(state: AgentState) -> int:
|
||||
"""使用 tiktoken(gpt-4o 编码器)计算当前上下文 token 数量。"""
|
||||
try:
|
||||
@@ -208,6 +277,7 @@ def count_tokens(state: AgentState) -> int:
|
||||
return len(enc.encode(text))
|
||||
|
||||
|
||||
@log_node("manage_context")
|
||||
def manage_context(state: AgentState) -> Dict:
|
||||
"""当 token 数量超过阈值时,压缩较早的对话轮次。"""
|
||||
token_count = count_tokens(state)
|
||||
@@ -230,7 +300,7 @@ def manage_context(state: AgentState) -> Dict:
|
||||
conv_text = json.dumps(older, ensure_ascii=False, indent=2)
|
||||
|
||||
try:
|
||||
llm = get_llm()
|
||||
llm = get_llm(caller="manage_context")
|
||||
prompt = load_prompt("compression").format(conversation_text=conv_text)
|
||||
resp = llm.invoke(prompt)
|
||||
new_compressed = resp.content.strip()[:300]
|
||||
@@ -249,6 +319,7 @@ def manage_context(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("load_session_node")
|
||||
def load_session_node(state: AgentState) -> Dict:
|
||||
"""在请求开始时从磁盘加载会话状态。"""
|
||||
session_id = state.get("session_id", "")
|
||||
@@ -273,6 +344,7 @@ def load_session_node(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("save_session_node")
|
||||
def save_session_node(state: AgentState) -> Dict:
|
||||
"""将当前代理状态持久化到磁盘。"""
|
||||
session_id = state.get("session_id", "")
|
||||
@@ -319,6 +391,7 @@ def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@log_node("retrieve")
|
||||
def retrieve(state: AgentState) -> Dict:
|
||||
"""在 ChromaDB + 错误知识库中搜索相关的 JRXML 模板和组件。"""
|
||||
try:
|
||||
@@ -341,12 +414,13 @@ def retrieve(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("generate")
|
||||
def generate(state: AgentState) -> Dict:
|
||||
"""根据用户需求和检索到的上下文生成初始 JRXML。"""
|
||||
from langgraph.config import get_stream_writer
|
||||
|
||||
writer = get_stream_writer()
|
||||
llm = get_llm()
|
||||
llm = get_llm(caller="generate")
|
||||
prompt = load_prompt("initial_generation").format(
|
||||
context=state.get("retrieved_context", ""),
|
||||
user_request=state.get("user_input", ""),
|
||||
@@ -361,12 +435,13 @@ def generate(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("modify_jrxml")
|
||||
def modify_jrxml(state: AgentState) -> Dict:
|
||||
"""根据用户的修改请求修改现有 JRXML。"""
|
||||
from langgraph.config import get_stream_writer
|
||||
|
||||
writer = get_stream_writer()
|
||||
llm = get_llm()
|
||||
llm = get_llm(caller="modify_jrxml")
|
||||
# 构建对话上下文:压缩摘要 + 最近对话
|
||||
compressed = state.get("compressed_history", "")
|
||||
recent = state.get("conversation_history", [])[-6:]
|
||||
@@ -405,6 +480,7 @@ def modify_jrxml(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("validate")
|
||||
def validate(state: AgentState) -> Dict:
|
||||
"""根据 FastAPI 验证服务验证当前 JRXML。"""
|
||||
jrxml = state.get("current_jrxml", "")
|
||||
@@ -448,9 +524,10 @@ def validate(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("explain_error")
|
||||
def explain_error(state: AgentState) -> Dict:
|
||||
"""生成验证错误的可读解释。"""
|
||||
llm = get_llm()
|
||||
llm = get_llm(caller="explain_error")
|
||||
jrxml = state.get("current_jrxml", "")
|
||||
lines = jrxml.split("\n")[:80]
|
||||
snippet = "\n".join(lines)
|
||||
@@ -464,12 +541,13 @@ def explain_error(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("correct_jrxml")
|
||||
def correct_jrxml(state: AgentState) -> Dict:
|
||||
"""尝试自动修正验证失败的 JRXML。"""
|
||||
from langgraph.config import get_stream_writer
|
||||
|
||||
writer = get_stream_writer()
|
||||
llm = get_llm()
|
||||
llm = get_llm(caller="correct_jrxml")
|
||||
prompt = load_prompt("correction").format(
|
||||
current_jrxml=state.get("current_jrxml", ""),
|
||||
error_msg=state.get("error_msg", ""),
|
||||
@@ -495,6 +573,7 @@ def correct_jrxml(state: AgentState) -> Dict:
|
||||
return state
|
||||
|
||||
|
||||
@log_node("finalize")
|
||||
def finalize(state: AgentState) -> Dict:
|
||||
"""保存最终验证通过的 JRXML 并更新对话历史 + 版本记录。"""
|
||||
jrxml = state.get("current_jrxml", "")
|
||||
|
||||
Reference in New Issue
Block a user