Skip to content

AI 对话日志系统 ​

1. 概述 ​

一次 /v1/chatbot/{agentId}/sessions/{sessionId}/chat 请求会在数据库里留下一棵"调用树",分三层:

层级表数量级含义
L1 会话ai_conversation_log1 次请求 1 条整次对话的元数据、最终响应、聚合成本
L2 节点ai_node_execution_log每次请求 N 条,树结构Agent 内部的执行节点(顶层图 + agentic_loop 子图 + 业务节点),支持父子关系与循环迭代
L3 调用ai_llm_call_log / ai_tool_call_log / ai_rag_call_log / ai_streaming_content_log每个 L2 节点 M 条每次 LLM / 工具 / RAG 调用的完整 prompt、参数、结果、tokens、错误

所有 L3 直接通过 node_execution_log_id 挂到具体的 L2 节点上,L2 通过 parent_node_execution_log_id 自关联形成树,L1 通过 trace_id 统一串联。

实时持久化由 RealtimeLogPersistenceListener 监听事件完成;业务代码不直接写 SQL。

2. 接口与 trace 入口映射 ​

接口 / 场景模式trace 启动位置备注
POST .../chat?sse=sync&stream=truesync SSEAOP 切面 ConversationLogAspect@ConversationLogging(streaming=true) 标注在 handleStreamChat
POST .../chat?stream=false非流式同上@ConversationLogging(streaming=false) 标注在 handleNormalChat
POST .../chat?sse=async&stream=true(默认)async SSEworker 线程CentralizedAgentMessageDispatcher.processMessage 里 TraceLifecycle.run
子 Agent TASK 消息workerCentralizedAgentMessageDispatcher.processTaskMessage 里 TraceLifecycle.run独立 trace
POST .../quick-chatSSEAgentChatServiceImpl.quickChatSse 里 TraceLifecycle.run一键对话
systemChatMessage非流式AgentChatServiceImpl.systemChatMessage 里 TraceLifecycle.run无 Security 上下文
Controller 前置拒绝(未登录 / Agent 不存在 / 会话失效 / maxLoops)短 traceAiChatbotAgentController.rejectChat → TraceLifecycle.rejectAndLog仅 L1 一条 FAILED,无 L2/L3

关键不变量:trace 一定 start,一定 end(SUCCESS/FAILED/TIMEOUT),一定 clear。所有入口统一走 TraceLifecycle.run 或 @ConversationLogging,避免手写 try/catch/finally 漏分支。

3. 数据模型 ​

3.1 L1 — ai_conversation_log ​

一次完整请求(用户 → Agent 响应)的顶层记录。

字段类型说明
idBIGINT主键
trace_idVARCHAR(64)链路追踪 ID,格式 trace-<8位uuid>-<timestamp>,全局唯一
session_idVARCHAR(64)会话 ID(外部可见的 sessionId)
agent_idBIGINT智能体 ID
agent_nameVARCHAR(255)智能体名称(冗余)
user_idBIGINT用户 ID(系统调用用 1 占位)
user_inputTEXT本次请求的用户消息
final_responseTEXTAgent 最终返回给用户的文本
statusVARCHAR(20)RUNNING / SUCCESS / FAILED / TIMEOUT
total_duration_msBIGINT总耗时
node_countINT本次请求经过的 L2 节点数
llm_call_countINT本次请求的 LLM 调用次数(全局聚合)
total_prompt_tokensINT全部 LLM 输入 token 总和
total_completion_tokensINT全部 LLM 输出 token 总和
total_tokensINT输入+输出 token
total_costDECIMAL(10,6)估算成本(USD)
workflow_typeVARCHAR(50)默认 autonomous_planning
is_streamingTINYINT(1)是否流式响应
first_token_latency_msBIGINT首 token 延迟(可选)
avg_token_latency_msBIGINT平均 token 延迟(可选)
error_messageTEXT失败时的错误消息
error_nodeVARCHAR(100)失败时的节点名
start_time / end_timeDATETIME时间戳

写入时机

  • INSERT(status=RUNNING):ConversationContext.start 发 ConversationStartedEvent 时,同步执行
  • UPDATE(status=SUCCESS/FAILED/TIMEOUT + 聚合字段):ConversationContext.end 发 ConversationCompletedEvent 时,异步执行(@Async("asyncExecutor"))

索引:trace_id、session_id、agent_id、user_id、start_time、status

3.2 L2 — ai_node_execution_log ​

Agent 内部的执行节点,支持父子关系和循环迭代。

字段类型说明
idBIGINT主键
trace_idVARCHAR(64)所属 L1 的 trace_id
conversation_log_idBIGINT所属 L1 的 id
parent_node_execution_log_idBIGINT父节点 id(自关联)。顶层节点为 NULL
node_nameVARCHAR节点展示名(中文,如 "推理决策")
node_typeVARCHAR严格枚举,见 §3.5
graph_nameVARCHAR(64)autonomous_planning / agentic_loop / NULL(业务节点)
execution_orderINT节点在 trace 内的执行序号
iterationINT循环迭代序号(reasoning/acting/refining 等循环节点),非循环为 NULL
statusVARCHAR(20)RUNNING / SUCCESS / FAILED / SKIPPED
duration_msBIGINT耗时
llm_call_count / prompt_tokens / completion_tokens / total_tokens / cost数字本节点自身直接消耗(不含子节点),聚合值用 SQL 递归 CTE 查询
state_before / state_afterLONGTEXT节点执行前/后的 state JSON
is_user_visibleTINYINT(1)是否对用户可见(决定是否推流)
start_time / end_timeDATETIME时间戳

写入时机

  • INSERT(status=RUNNING):ConversationContext.startNode 发 NodeStartedEvent,同步执行,立即回写 dbId 给内存 NodeExecution
  • UPDATE(status + stateAfter + 聚合):ConversationContext.endNode 发 NodeCompletedEvent,异步执行

索引:trace_id、conversation_log_id、parent_node_execution_log_id、graph_name

3.3 L3 — LLM 调用 ai_llm_call_log ​

每次 LLM 请求/响应。

字段类型说明
idBIGINT主键
trace_id / conversation_log_id / node_execution_log_id关联挂到具体 L2 节点
call_typeVARCHAR(20)normal / streaming / param_repair / intent_recognition / planning / memory_compress / summarize — 调用意图
model_providerVARCHAR(50)openai / anthropic / zhipu / deepseek / SUB_AGENT / unknown
model_nameVARCHAR(100)模型名
message_countINT本次请求传入的消息数
system_prompt / user_message / ai_responseLONGTEXT完整 prompt / 完整响应(上限 10k/10k/5k)
prompt_tokens / completion_tokens / total_tokensINT本次调用的 tokens
estimated_costDOUBLEUSD
tool_countINT本次请求可调用的工具数量
tool_namesVARCHAR(500)可用工具名列表
tool_calls_countINTLLM 本次响应要求的工具调用次数
tool_calls_detailLONGTEXTLLM 返回的 tool_calls JSON
reasoning_length / reasoning_contentINT / LONGTEXTClaude/Qwen 等的 thinking 内容
call_sourceVARCHAR(100)调用来源节点名(= node_execution_log.node_name)
call_start_time / call_end_time / latency_ms时间调用耗时
statusVARCHAR(20)SUCCESS / FAILED
finish_reasonVARCHAR(50)STOP / LENGTH / TOOL_EXECUTION — 决定 Agentic Loop 的路由
error_type / error_message失败时填

写入时机:LlmCallCompletedEvent 同步 INSERT。listener 发生位置:

  • 非流式:SmartLlmCallListener.onResponse / onError
  • 流式:SmartStreamingChatModelWrapper 的 onCompleteResponse / onError
  • AiService 调用包装层:AiServiceStartedListenerImpl / AiServiceCompletedListenerImpl / AiServiceErrorListenerImpl → ConversationContext.recordAgentCall → 转 LlmCall(modelProvider=SUB_AGENT)

关于双重记录:上述 Smart*Listener(ChatModelListener)记录每次底层 LLM 物理调用;AiService*Listener(langchain4j observability)记录每次 AiService 调用包装。一次 AiService 调用可能内含多次 ChatModel 调用(带工具时),因此两类记录维度不同,保留双记录便于按"业务调用单元"和"物理 LLM 调用"分别统计。AiService* 那条用 model_provider=SUB_AGENT 区分。

3.4 L3 — 工具调用 ai_tool_call_log ​

MCP 工具、VFS shell、Sandbox、DocSpace、子 Agent、技能、check_task_status、缓存命中都落这张表,通过 executor_type 区分。

字段类型说明
idBIGINT主键
trace_id / conversation_log_id / node_execution_log_id关联
llm_call_idBIGINT触发本次工具调用的 LLM 调用 id(LLM 通过 tool_calls 返回)
attempt_indexINT重试序号(从 1 开始),NULL 表示非重试上下文
executor_typeVARCHAR(50)见 §3.6
tool_id / tool_nameVARCHAR工具标识
tool_endpoint / nacos_service_nameVARCHARMCP 工具专有
input_paramsJSON/LONGTEXT输入参数,sandbox/vfs 分支会额外塞 _resolvedCommand 记录实际执行的 shell 命令串
output_resultLONGTEXT原始返回
call_sourceVARCHAR来源节点名
call_start_time / call_end_time / latency_ms
statusVARCHARSUCCESS / FAILED
error_type / error_message

写入时机:ToolCallRecordedEvent 异步 INSERT。

3.5 L3 — RAG 检索 ai_rag_call_log ​

字段说明
trace_id / conversation_log_id / node_execution_log_id关联
query_text检索查询
knowledge_base_idsJSON 数组
retrieved_count召回分段数
rerank_enabled是否启用重排
max_similarity / avg_similarity相似度分布
retrieval_summary文本摘要
retrieval_detail完整 chunk JSON
call_source / 时间戳 / status / 错误同上

写入时机:RagCallRecordedEvent 异步 INSERT。

3.6 L3 — 流式内容 ai_streaming_content_log ​

字段说明
trace_id / conversation_log_id / node_execution_log_id关联
typetoken / thinking / tool_call / agentic_loop / ...
content推流内容
sequence该节点内的推送序号
timestamp

由 StreamingContentLoggingListener 监听 StreamingContentEvent 异步落库。

4. 枚举值 ​

4.1 NodeType(L2 节点类型) ​

定义在 com.pig4cloud.pigx.ai.logging.model.NodeType,严格控制 ai_node_execution_log.node_type 写入值。

值用于哪些节点
PIPELINE顶层图:load_config / intent_recognition / planning / agentic_loop / task_end
LOOP_INITAgentic Loop 初始化
LOOP_REASONINGAgentic Loop 推理决策(循环,有 iteration)
LOOP_ACTINGAgentic Loop 执行动作(循环,有 iteration)
LOOP_REFININGAgentic Loop 结果评估(循环,有 iteration)
LOOP_MEMORYAgentic Loop 上下文压缩
HUMAN_REVIEW人工介入(HITL)
SESSION会话管理节点:session_init / session_validate
KNOWLEDGE_BASEkb_load(知识库配置加载)
DRAFT_WRITEBACKVFS 文件落盘后回填草稿记录

4.2 LLM call_type(调用意图) ​

值场景
normal非流式业务主调用
streaming流式业务主调用
param_repairToolCallNode.analyzeAndFixParams 调 LLM 修正工具参数
intent_recognition意图识别节点
planning任务规划节点
memory_compress上下文压缩节点
summarize汇总节点

设置方式:LlmCallIntentContext.withIntent("param_repair", () -> chatModel.chat(...))。未设置时走默认 normal / streaming。

4.3 工具 executor_type ​

按"动作类型"分类。Agentic Loop 的 acting 节点下每一条工具调用都会落到这张表。

值含义入口
MCP_NACOSNacos 注册的 MCP 工具AbstractToolExecutor 子类
MCP_REMOTE远程 MCP 工具同上
LOCAL本地 Java 工具 / check_task_status同上
DEFAULT兜底类型同上
SKILL技能调用(skill_call 工具)SkillCallNode
SUB_AGENT子 Agent 调用(sub_agent_call / submit_subagent_task)SubAgentCallNode
VFS虚拟文件系统 shell 命令(纯内存或 DocSpace 挂载模式)LoopActingNode.dispatchToolCall
SANDBOXSandbox 容器 shell 命令LoopActingNode.dispatchSandboxTool
DOC_SPACEDocSpace 文档工具(get-document-structure / get-page-content / get-page-image / get-page-table / search-relevant-pages)LoopActingNode.dispatchDocSpaceToolIfMatch
CACHED工具结果缓存命中(未真正执行)LoopActingNode cache hit 分支

5. 运行时核心类 ​

com.pig4cloud.pigx.ai.logging
├── context/
│   ├── ConversationContext          # 核心:trace 栈、start/end、recordXxxCall
│   ├── TraceLifecycle               # 统一 trace 生命周期工具
│   ├── LlmCallIntentContext         # LLM call_type 标签 (ThreadLocal)
│   ├── ToolCallAttemptContext       # 工具重试序号 (ThreadLocal)
│   └── EventPublisherHolder         # 事件发布器持有
├── concurrent/
│   └── TraceAwareExecutor           # 包装 ExecutorService,自动 snapshot/restore trace
├── annotation/
│   └── @ConversationLogging         # AOP 注解,标在 service 方法上自动 start/end
├── aspect/
│   └── ConversationLogAspect        # @ConversationLogging 的 AOP 实现
├── decorator/
│   ├── TrackedNodeAction            # 包装 langgraph4j AsyncNodeAction,自动 start/endNode
│   └── LoggingStreamingResponseHandler  # 流式 response handler 装饰器
├── listener/
│   ├── RealtimeLogPersistenceListener   # 实时持久化:订阅事件写 DB
│   ├── SmartLlmCallListener             # langchain4j ChatModelListener,记 LLM 调用
│   ├── SmartStreamingChatModelWrapper   # 流式 ChatModel 包装
│   ├── SmartToolExecutionListener       # langchain4j ToolExecutor 监听
│   ├── SmartContentRetrieverListener    # RAG ContentRetriever 监听
│   ├── SmartEmbeddingModelListener      # embedding 调用监听
│   ├── AiServiceStarted/Completed/ErrorListenerImpl  # AiService 生命周期
│   └── StreamingContentLoggingListener  # 流式推送内容落库
├── event/                          # 对应各生命周期的 Spring ApplicationEvent
├── model/                          # 内存模型:ConversationTrace / NodeExecution / LlmCall / ToolExecutionCall / RagExecutionCall
└── util/
    ├── LoggingConstants
    └── LoggingUtils

5.1 ConversationContext ​

ThreadLocal 存储

  • CONTEXT: InheritableThreadLocal<ConversationTrace> — 当前 trace
  • CURRENT_LLM_CALL: InheritableThreadLocal<LlmCall> — 最近一次 LLM 调用(用于把工具调用关联到触发它的 LLM)

节点栈(关键) 每个 ConversationTrace 内部持有 Deque<NodeExecution> nodeStack。startNode push、endNode pop。栈顶 = 当前活跃节点。L3 记录时直接从栈顶取 dbId 绑到 call,避免按 nodeName 反查。

主要 API

java
// 生命周期
ConversationTrace start(sessionId, agentId, userId, isStreaming, userInput);
ConversationTrace end(status, finalResponse, errorMessage);
void clear();

// 节点
NodeExecution startNode(nodeName, NodeType, graphName, iteration, isUserVisible, stateBefore);
NodeExecution startNode(nodeName, NodeType, graphName, isUserVisible);      // 无 iteration
NodeExecution startNode(nodeName, NodeType, isUserVisible);                 // 最简
void endNode(NodeExecution node, status, stateAfter);
<T> T withNode(name, NodeType, isUserVisible, Supplier<T>);                 // 自动成对调用

// L3 记录
void recordLlmCall(LlmCall);
void recordToolCall(ToolExecutionCall);
void recordRagCall(RagExecutionCall);
void recordAgentCall(AgentExecutionCall);                                   // 转 LlmCall 落库

// 跨线程
Snapshot snapshot();
void restore(Snapshot);
void set(trace);

5.2 TraceLifecycle ​

统一"start → try → end → clear"模板,避免每个调用点手写 try/catch/finally 出错。

java
// 开 trace 运行任务,异常自动 FAILED
<T> T run(sessionId, agentId, userId, streaming, userInput, Supplier<T>);
void run(sessionId, agentId, userId, streaming, userInput, Runnable);

// 在已有 trace 上运行(snapshot/restore 之后),不 start
<T> T runWithTrace(ConversationTrace, Supplier<T>);

// 前置拒绝:start → end(FAILED) → clear,不执行业务
void rejectAndLog(sessionId, agentId, userId, streaming, userInput, reason);

5.3 TraceAwareExecutor ​

装饰 ExecutorService,submit 时自动 snapshot 提交线程的 trace,worker 线程自动 restore → try → clear。适用于:

  • 普通线程池(Executors.newCachedThreadPool())
  • 虚拟线程(Thread.ofVirtual().factory())
java
ExecutorService wrapped = TraceAwareExecutor.wrap(delegate);

已经在以下位置使用:

  • AgentChatServiceImpl.executorService(流式/quickChat 的任务线程池)
  • CentralizedAgentMessageDispatcher.createSessionExecutor(per-session 虚拟线程执行器)

5.4 TrackedNodeAction ​

langgraph4j 节点装饰器。进入节点 startNode,whenComplete 时 endNode。支持循环节点的 iteration 自动填充。

java
// 一次性节点(pipeline / session / ...)
TrackedNodeAction.wrap(action, nodeName, NodeType, graphName, isUserVisible);

// 循环节点(传 iteration 提取器)
TrackedNodeAction.wrap(action, nodeName, NodeType, graphName, isUserVisible,
                      state -> state.iterationCount());

6. 执行流程(智能体循环完整示例) ​

以 POST /v1/chatbot/{agentId}/sessions/new/chat?sse=async 为例,trace 树如下:

L1 ai_conversation_log (trace-xxx)
 │
 ├─ L2 session_init              (NodeType=SESSION,        graph=null)
 ├─ L2 session_validate          (NodeType=SESSION,        graph=null)   ← sessionId=new 时无
 ├─ L2 kb_load                   (NodeType=KNOWLEDGE_BASE, graph=null)
 │
 ├─ L2 加载智能体配置              (NodeType=PIPELINE,       graph=autonomous_planning)
 │    └─ (无 L3)
 │
 ├─ L2 意图识别                   (NodeType=PIPELINE,       graph=autonomous_planning)
 │    └─ L3 ai_llm_call_log (call_type=intent_recognition)
 │
 ├─ L2 任务规划                   (NodeType=PIPELINE,       graph=autonomous_planning)
 │    └─ L3 ai_llm_call_log (call_type=planning)
 │
 ├─ L2 任务执行                   (NodeType=PIPELINE,       graph=autonomous_planning)
 │    │
 │    ├─ L2 自规划任务初始化       (NodeType=LOOP_INIT,      graph=agentic_loop, iter=null)
 │    │
 │    ├─ L2 推理决策              (NodeType=LOOP_REASONING, graph=agentic_loop, iter=1)
 │    │    └─ L3 ai_llm_call_log (call_type=streaming, tool_calls_count=2)
 │    │
 │    ├─ L2 执行动作              (NodeType=LOOP_ACTING,    graph=agentic_loop, iter=1)
 │    │    ├─ L3 ai_tool_call_log (executor=MCP_NACOS, tool=search_db,  attempt=1,  FAILED)
 │    │    ├─ L3 ai_llm_call_log  (call_type=param_repair)              ← 修正参数
 │    │    ├─ L3 ai_tool_call_log (executor=MCP_NACOS, tool=search_db,  attempt=2,  SUCCESS)
 │    │    └─ L3 ai_rag_call_log  (query=..., retrievedCount=5)
 │    │
 │    ├─ L2 结果评估              (NodeType=LOOP_REFINING,  graph=agentic_loop, iter=1)
 │    │    └─ L3 ai_llm_call_log
 │    │
 │    ├─ L2 推理决策              (NodeType=LOOP_REASONING, graph=agentic_loop, iter=2)
 │    ├─ L2 执行动作              (NodeType=LOOP_ACTING,    graph=agentic_loop, iter=2)
 │    │    ├─ L3 ai_tool_call_log (executor=SUB_AGENT, tool=sub_agent_call)
 │    │    ├─ L3 ai_tool_call_log (executor=VFS, tool=write, _resolvedCommand="write \"/a.md\" \"...\"")
 │    │    └─ L3 ai_tool_call_log (executor=SKILL, tool=skill_call)
 │    ├─ L2 结果评估              (NodeType=LOOP_REFINING,  graph=agentic_loop, iter=2)
 │    ...
 │
 └─ L2 用户反馈                   (NodeType=PIPELINE,       graph=autonomous_planning)
      └─ L3 ai_llm_call_log

7. 执行动作(ACTING)详细记录覆盖 ​

Agentic Loop 的 ACTING 节点下所有"动作"都有 L3 记录:

动作入口executor_type补充字段
RAG 检索rag_search 工具 → RagNode(独立 ai_rag_call_log)query、KB、相似度、rerank、detail
MCP / 本地工具ToolCallNode.executeDirect → AbstractToolExecutorMCP_NACOS / MCP_REMOTE / LOCAL / DEFAULTattempt_index
技能调用skill_call 工具 → SkillCallNodeSKILLskillId、resolvedSkillName、完整 markdown
子 Agent 调用sub_agent_call / submit_subagent_task → SubAgentCallNodeSUB_AGENTsubAgentId、streaming、输入输出
VFS shellcat / read / write / grep / ... → VirtualShellVFS_resolvedCommand = 实际执行的 shell 命令串
Sandbox shell同上但沙箱启用SANDBOX_resolvedCommand
DocSpace 文档工具get-document-structure / get-page-content / get-page-image / get-page-table / search-relevant-pagesDOC_SPACEdoc_id / pages / page_idx / query
任务状态查询check_task_status → CheckTaskStatusNodeLOCALtaskId、状态
缓存命中任意工具命中 toolResultStoreCACHEDduration=0
LLM 业务调用所有 chatModel.chat/streamingChat(ai_llm_call_log)call_type=normal/streaming
LLM 参数修正ToolCallNode.analyzeAndFixParams(ai_llm_call_log)call_type=param_repair

触发高风险操作时的 HITL 原因:写在 ACTING 节点 state_after.HITL_REQUEST 里,包含 triggerReason("rm recursive" / "shell pattern: DELETE FROM" 等)、toolName、toolParams、iteration。

8. 控制器前置拒绝日志 ​

AiChatbotAgentController.chat 早退分支统一走 rejectChat → TraceLifecycle.rejectAndLog,每种拒绝原因都登记一行 L1 FAILED:

拒绝原因error_message
未登录未登录
Agent 不存在Agent不存在
会话验证失败会话验证失败:会话不存在、已失效或无权访问
会话失效会话不存在或已失效
达到最大对话轮数已达到最大对话轮数,请创建新会话
未知 SSE 模式未知的 SSE 模式:...
顶层异常对话失败: ...

这类 trace 只有 L1 一条,无 L2/L3。

9. 实时持久化与最终聚合 ​

所有写 DB 都由 RealtimeLogPersistenceListener 订阅 Spring ApplicationEvent 完成:

事件触发处理方式操作
ConversationStartedEventstart同步INSERT L1 (RUNNING),回写 trace.dbId
ConversationCompletedEventend异步 @Async("asyncExecutor")UPDATE L1 聚合字段
NodeStartedEventstartNode同步INSERT L2 (RUNNING),回写 node.dbId
NodeCompletedEventendNode异步UPDATE L2 状态和耗时
LlmCallCompletedEventrecordLlmCall异步INSERT ai_llm_call_log(使用 llmCall.nodeExecutionLogId)
ToolCallRecordedEventrecordToolCall异步INSERT ai_tool_call_log(使用 toolCall.nodeRef.dbId)
RagCallRecordedEventrecordRagCall异步INSERT ai_rag_call_log(使用 ragCall.nodeRef.dbId)
StreamingContentEvent流式推送异步(StreamingContentLoggingListener)INSERT ai_streaming_content_log

为什么 start 是同步而 completed 是异步:dbId 需要立即回写到内存对象,后续的 UPDATE / 子节点 INSERT / L3 INSERT 都靠它。completed 不依赖回写,用异步不阻塞业务路径。

错挂 bug 修复:之前按 call_source(nodeName 字符串)反查 node dbId,在循环节点多轮同名场景下总挂到第一轮。现在 L3 记录时直接从栈顶节点取 dbId(NodeExecution.getDbId()),绑到 call 的 nodeRef 或 nodeExecutionLogId,listener 只读不反查。

10. 典型查询 SQL ​

查一次请求的完整节点树(递归 CTE)

sql
WITH RECURSIVE node_tree AS (
  SELECT id, parent_node_execution_log_id, node_name, node_type, graph_name, iteration,
         status, duration_ms, total_tokens, cost, 0 AS depth
  FROM ai_node_execution_log
  WHERE conversation_log_id = ? AND parent_node_execution_log_id IS NULL
  UNION ALL
  SELECT c.id, c.parent_node_execution_log_id, c.node_name, c.node_type, c.graph_name, c.iteration,
         c.status, c.duration_ms, c.total_tokens, c.cost, t.depth + 1
  FROM ai_node_execution_log c
  JOIN node_tree t ON c.parent_node_execution_log_id = t.id
)
SELECT * FROM node_tree ORDER BY depth, id;

聚合一次请求真实成本(含所有子调用 tokens)

sql
SELECT
  (SELECT SUM(total_tokens) FROM ai_llm_call_log WHERE conversation_log_id = ?) AS total_tokens,
  (SELECT SUM(estimated_cost) FROM ai_llm_call_log WHERE conversation_log_id = ?) AS total_cost;

查一个 agentic_loop 里 ACTING 每一轮的工具调用

sql
SELECT n.iteration, t.tool_name, t.executor_type, t.attempt_index,
       t.status, t.latency_ms, JSON_EXTRACT(t.input_params, '$._resolvedCommand') AS resolved_cmd
FROM ai_node_execution_log n
JOIN ai_tool_call_log t ON t.node_execution_log_id = n.id
WHERE n.conversation_log_id = ?
  AND n.graph_name = 'agentic_loop'
  AND n.node_type = 'LOOP_ACTING'
ORDER BY n.iteration, t.call_start_time;

查一次请求里所有"参数修正"类型的 LLM 调用

sql
SELECT l.call_start_time, l.model_name, l.total_tokens, l.ai_response, l.error_message
FROM ai_llm_call_log l
WHERE l.conversation_log_id = ? AND l.call_type = 'param_repair'
ORDER BY l.call_start_time;

查哪些 trace 因为哪个高风险操作触发了 HITL

sql
SELECT c.trace_id, n.iteration,
       JSON_EXTRACT(n.state_after, '$.HITL_REQUEST.triggerReason') AS reason,
       JSON_EXTRACT(n.state_after, '$.HITL_REQUEST.toolName') AS tool
FROM ai_conversation_log c
JOIN ai_node_execution_log n ON n.conversation_log_id = c.id
WHERE n.node_type = 'LOOP_ACTING'
  AND JSON_EXTRACT(n.state_after, '$.HITL_PENDING') = true;

查工具重试链

sql
SELECT trace_id, tool_name, attempt_index, status, error_message, latency_ms
FROM ai_tool_call_log
WHERE conversation_log_id = ? AND attempt_index IS NOT NULL
ORDER BY tool_name, call_start_time;

11. 写新节点 / 新动作的接入规范 ​

11.1 业务节点(主动调用型,非 langgraph4j 图节点) ​

AgentChatServiceImpl.loadDatasetsForSession 式的代码:

java
ConversationContext.withNode("kb_load", NodeType.KNOWLEDGE_BASE, false, () -> {
    // 业务逻辑
});

异常自动标 FAILED,成功自动 SUCCESS,无需写 try/finally。

11.2 langgraph4j 图节点 ​

java
graph.addNode(SubNode.MY_NODE.value,
    TrackedNodeAction.wrap(node_async(myNode::apply), "我的节点",
        NodeType.LOOP_XXX, "agentic_loop", true,
        state -> state.iterationCount()));   // 循环节点才传 iteration extractor

11.3 新增一种"动作"类型的 L3 记录 ​

如果动作由 LoopActingNode.dispatchToolCall 触发但没有委托给已带埋点的子节点:

java
return recordedDispatch(toolName, "MY_EXECUTOR_TYPE", params, resolvedCommand,
        () -> myService.execute(params));

如果动作由独立节点内触发(像 SkillCallNode / SubAgentCallNode),直接 new 一个 ToolExecutionCall,try/finally 中 ConversationContext.recordToolCall(toolCall)。

11.4 跨线程执行 L3 调用 ​

普通线程池:用 TraceAwareExecutor.wrap(executor)。

手动跨线程:

java
ConversationContext.Snapshot snapshot = ConversationContext.snapshot();
someOtherThread.execute(() -> {
    ConversationContext.restore(snapshot);
    try {
        // 里面调 recordXxxCall
    } finally {
        ConversationContext.clear();
    }
});

11.5 标记 LLM 调用意图 ​

java
LlmCallIntentContext.withIntent(LlmCallIntentContext.INTENT_PARAM_REPAIR, () -> {
    return chatModel.chat(request);
});

12. 数据库迁移 ​

版本文件内容
V0.0.13V0.0.13_ai_logging.sql初始三层表结构:ai_conversation_log / ai_node_execution_log / ai_llm_call_log
V0.0.14V0.0.14__refactor_logging_system.sql节点表加 state_before/state_after;拆分 LLM/Tool/RAG 三张调用表
V0.0.15V0.0.15__create_streaming_content_log.sql新增 ai_streaming_content_log
V0.0.16V0.0.16__add_system_prompt_to_llm_call_log.sqlLLM 调用日志拆分 system_prompt / user_message
V0.0.25V0.0.25__refactor_logging_tree.sql节点表加 parent_node_execution_log_id / iteration / graph_name
V0.0.26V0.0.26__tool_call_attempt_index.sql工具调用日志加 attempt_index,executor_type 注释扩展

13. 已知约束与注意事项 ​

  • ConversationContext 的 ThreadLocal 是 InheritableThreadLocal。普通 new Thread() 会继承;但线程池场景由于线程复用,必须用 TraceAwareExecutor 或 snapshot/restore。虚拟线程(Thread.ofVirtual())默认不继承 InheritableThreadLocal,必须走 TraceAwareExecutor。
  • RealtimeLogPersistenceListener.onConversationStarted 和 onNodeStarted 是同步监听器,因为需要立即回写 dbId。不要改成 @Async,否则后续 UPDATE / 子节点 INSERT 会因 dbId 为 null 而失败。
  • L3 node_execution_log_id 在 listener 写 DB 时直接取自 call 对象,不再反查。如果自定义新的 L3 记录路径绕过 ConversationContext.recordXxxCall,要自行确保 nodeRef 或 nodeExecutionLogId 已设置。
  • TrackedNodeAction 只对顶层 langgraph4j graph 节点起装饰作用。Agentic Loop 的 ACTING 节点内部通过 executeDelegate 同步调用 RagNode / SkillCallNode / SubAgentCallNode / CheckTaskStatusNode 不会产生新的 L2 节点——这些动作都作为 L3 挂在 ACTING 节点下,更符合树形语义(否则栈嵌套会让父子关系颠倒)。
  • 子 Agent 在当前实现中是同进程同线程同步调用(LoopActingNode.dispatchToolCall("sub_agent_call") → SubAgentCallNode.apply() → AiAgentExecutionService.executeAgent),运行在父 trace 的上下文里,作为一条 L3 ToolExecutionCall(SUB_AGENT) 记录。没有独立的子 trace。
  • sendSubagentTask / sendSubagentMessage 是另一套走 MQ dispatcher 的异步任务系统(AgentMessage.MessageType.TASK),仅被 SubagentTaskTool 使用,不在主 chat 路径上。这条路径下会产生独立子 trace。