Appearance
AI 对话日志系统
1. 概述
一次 /v1/chatbot/{agentId}/sessions/{sessionId}/chat 请求会在数据库里留下一棵"调用树",分三层:
| 层级 | 表 | 数量级 | 含义 |
|---|---|---|---|
| L1 会话 | ai_conversation_log | 1 次请求 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=true | sync SSE | AOP 切面 ConversationLogAspect | @ConversationLogging(streaming=true) 标注在 handleStreamChat |
POST .../chat?stream=false | 非流式 | 同上 | @ConversationLogging(streaming=false) 标注在 handleNormalChat |
POST .../chat?sse=async&stream=true(默认) | async SSE | worker 线程 | CentralizedAgentMessageDispatcher.processMessage 里 TraceLifecycle.run |
子 Agent TASK 消息 | worker | CentralizedAgentMessageDispatcher.processTaskMessage 里 TraceLifecycle.run | 独立 trace |
POST .../quick-chat | SSE | AgentChatServiceImpl.quickChatSse 里 TraceLifecycle.run | 一键对话 |
systemChatMessage | 非流式 | AgentChatServiceImpl.systemChatMessage 里 TraceLifecycle.run | 无 Security 上下文 |
| Controller 前置拒绝(未登录 / Agent 不存在 / 会话失效 / maxLoops) | 短 trace | AiChatbotAgentController.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 响应)的顶层记录。
| 字段 | 类型 | 说明 |
|---|---|---|
id | BIGINT | 主键 |
trace_id | VARCHAR(64) | 链路追踪 ID,格式 trace-<8位uuid>-<timestamp>,全局唯一 |
session_id | VARCHAR(64) | 会话 ID(外部可见的 sessionId) |
agent_id | BIGINT | 智能体 ID |
agent_name | VARCHAR(255) | 智能体名称(冗余) |
user_id | BIGINT | 用户 ID(系统调用用 1 占位) |
user_input | TEXT | 本次请求的用户消息 |
final_response | TEXT | Agent 最终返回给用户的文本 |
status | VARCHAR(20) | RUNNING / SUCCESS / FAILED / TIMEOUT |
total_duration_ms | BIGINT | 总耗时 |
node_count | INT | 本次请求经过的 L2 节点数 |
llm_call_count | INT | 本次请求的 LLM 调用次数(全局聚合) |
total_prompt_tokens | INT | 全部 LLM 输入 token 总和 |
total_completion_tokens | INT | 全部 LLM 输出 token 总和 |
total_tokens | INT | 输入+输出 token |
total_cost | DECIMAL(10,6) | 估算成本(USD) |
workflow_type | VARCHAR(50) | 默认 autonomous_planning |
is_streaming | TINYINT(1) | 是否流式响应 |
first_token_latency_ms | BIGINT | 首 token 延迟(可选) |
avg_token_latency_ms | BIGINT | 平均 token 延迟(可选) |
error_message | TEXT | 失败时的错误消息 |
error_node | VARCHAR(100) | 失败时的节点名 |
start_time / end_time | DATETIME | 时间戳 |
写入时机
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 内部的执行节点,支持父子关系和循环迭代。
| 字段 | 类型 | 说明 |
|---|---|---|
id | BIGINT | 主键 |
trace_id | VARCHAR(64) | 所属 L1 的 trace_id |
conversation_log_id | BIGINT | 所属 L1 的 id |
parent_node_execution_log_id | BIGINT | 父节点 id(自关联)。顶层节点为 NULL |
node_name | VARCHAR | 节点展示名(中文,如 "推理决策") |
node_type | VARCHAR | 严格枚举,见 §3.5 |
graph_name | VARCHAR(64) | autonomous_planning / agentic_loop / NULL(业务节点) |
execution_order | INT | 节点在 trace 内的执行序号 |
iteration | INT | 循环迭代序号(reasoning/acting/refining 等循环节点),非循环为 NULL |
status | VARCHAR(20) | RUNNING / SUCCESS / FAILED / SKIPPED |
duration_ms | BIGINT | 耗时 |
llm_call_count / prompt_tokens / completion_tokens / total_tokens / cost | 数字 | 本节点自身直接消耗(不含子节点),聚合值用 SQL 递归 CTE 查询 |
state_before / state_after | LONGTEXT | 节点执行前/后的 state JSON |
is_user_visible | TINYINT(1) | 是否对用户可见(决定是否推流) |
start_time / end_time | DATETIME | 时间戳 |
写入时机
INSERT(status=RUNNING):ConversationContext.startNode发NodeStartedEvent,同步执行,立即回写dbId给内存NodeExecutionUPDATE(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 请求/响应。
| 字段 | 类型 | 说明 |
|---|---|---|
id | BIGINT | 主键 |
trace_id / conversation_log_id / node_execution_log_id | 关联 | 挂到具体 L2 节点 |
call_type | VARCHAR(20) | normal / streaming / param_repair / intent_recognition / planning / memory_compress / summarize — 调用意图 |
model_provider | VARCHAR(50) | openai / anthropic / zhipu / deepseek / SUB_AGENT / unknown |
model_name | VARCHAR(100) | 模型名 |
message_count | INT | 本次请求传入的消息数 |
system_prompt / user_message / ai_response | LONGTEXT | 完整 prompt / 完整响应(上限 10k/10k/5k) |
prompt_tokens / completion_tokens / total_tokens | INT | 本次调用的 tokens |
estimated_cost | DOUBLE | USD |
tool_count | INT | 本次请求可调用的工具数量 |
tool_names | VARCHAR(500) | 可用工具名列表 |
tool_calls_count | INT | LLM 本次响应要求的工具调用次数 |
tool_calls_detail | LONGTEXT | LLM 返回的 tool_calls JSON |
reasoning_length / reasoning_content | INT / LONGTEXT | Claude/Qwen 等的 thinking 内容 |
call_source | VARCHAR(100) | 调用来源节点名(= node_execution_log.node_name) |
call_start_time / call_end_time / latency_ms | 时间 | 调用耗时 |
status | VARCHAR(20) | SUCCESS / FAILED |
finish_reason | VARCHAR(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 区分。
| 字段 | 类型 | 说明 |
|---|---|---|
id | BIGINT | 主键 |
trace_id / conversation_log_id / node_execution_log_id | 关联 | |
llm_call_id | BIGINT | 触发本次工具调用的 LLM 调用 id(LLM 通过 tool_calls 返回) |
attempt_index | INT | 重试序号(从 1 开始),NULL 表示非重试上下文 |
executor_type | VARCHAR(50) | 见 §3.6 |
tool_id / tool_name | VARCHAR | 工具标识 |
tool_endpoint / nacos_service_name | VARCHAR | MCP 工具专有 |
input_params | JSON/LONGTEXT | 输入参数,sandbox/vfs 分支会额外塞 _resolvedCommand 记录实际执行的 shell 命令串 |
output_result | LONGTEXT | 原始返回 |
call_source | VARCHAR | 来源节点名 |
call_start_time / call_end_time / latency_ms | ||
status | VARCHAR | SUCCESS / 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_ids | JSON 数组 |
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 | 关联 |
type | token / 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_INIT | Agentic Loop 初始化 |
LOOP_REASONING | Agentic Loop 推理决策(循环,有 iteration) |
LOOP_ACTING | Agentic Loop 执行动作(循环,有 iteration) |
LOOP_REFINING | Agentic Loop 结果评估(循环,有 iteration) |
LOOP_MEMORY | Agentic Loop 上下文压缩 |
HUMAN_REVIEW | 人工介入(HITL) |
SESSION | 会话管理节点:session_init / session_validate |
KNOWLEDGE_BASE | kb_load(知识库配置加载) |
DRAFT_WRITEBACK | VFS 文件落盘后回填草稿记录 |
4.2 LLM call_type(调用意图)
| 值 | 场景 |
|---|---|
normal | 非流式业务主调用 |
streaming | 流式业务主调用 |
param_repair | ToolCallNode.analyzeAndFixParams 调 LLM 修正工具参数 |
intent_recognition | 意图识别节点 |
planning | 任务规划节点 |
memory_compress | 上下文压缩节点 |
summarize | 汇总节点 |
设置方式:LlmCallIntentContext.withIntent("param_repair", () -> chatModel.chat(...))。未设置时走默认 normal / streaming。
4.3 工具 executor_type
按"动作类型"分类。Agentic Loop 的 acting 节点下每一条工具调用都会落到这张表。
| 值 | 含义 | 入口 |
|---|---|---|
MCP_NACOS | Nacos 注册的 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 |
SANDBOX | Sandbox 容器 shell 命令 | LoopActingNode.dispatchSandboxTool |
DOC_SPACE | DocSpace 文档工具(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
└── LoggingUtils5.1 ConversationContext
ThreadLocal 存储
CONTEXT: InheritableThreadLocal<ConversationTrace>— 当前 traceCURRENT_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_log7. 执行动作(ACTING)详细记录覆盖
Agentic Loop 的 ACTING 节点下所有"动作"都有 L3 记录:
| 动作 | 入口 | executor_type | 补充字段 |
|---|---|---|---|
| RAG 检索 | rag_search 工具 → RagNode | (独立 ai_rag_call_log) | query、KB、相似度、rerank、detail |
| MCP / 本地工具 | ToolCallNode.executeDirect → AbstractToolExecutor | MCP_NACOS / MCP_REMOTE / LOCAL / DEFAULT | attempt_index |
| 技能调用 | skill_call 工具 → SkillCallNode | SKILL | skillId、resolvedSkillName、完整 markdown |
| 子 Agent 调用 | sub_agent_call / submit_subagent_task → SubAgentCallNode | SUB_AGENT | subAgentId、streaming、输入输出 |
| VFS shell | cat / read / write / grep / ... → VirtualShell | VFS | _resolvedCommand = 实际执行的 shell 命令串 |
| Sandbox shell | 同上但沙箱启用 | SANDBOX | _resolvedCommand |
| DocSpace 文档工具 | get-document-structure / get-page-content / get-page-image / get-page-table / search-relevant-pages | DOC_SPACE | doc_id / pages / page_idx / query |
| 任务状态查询 | check_task_status → CheckTaskStatusNode | LOCAL | taskId、状态 |
| 缓存命中 | 任意工具命中 toolResultStore | CACHED | duration=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 完成:
| 事件 | 触发 | 处理方式 | 操作 |
|---|---|---|---|
ConversationStartedEvent | start | 同步 | INSERT L1 (RUNNING),回写 trace.dbId |
ConversationCompletedEvent | end | 异步 @Async("asyncExecutor") | UPDATE L1 聚合字段 |
NodeStartedEvent | startNode | 同步 | INSERT L2 (RUNNING),回写 node.dbId |
NodeCompletedEvent | endNode | 异步 | UPDATE L2 状态和耗时 |
LlmCallCompletedEvent | recordLlmCall | 异步 | INSERT ai_llm_call_log(使用 llmCall.nodeExecutionLogId) |
ToolCallRecordedEvent | recordToolCall | 异步 | INSERT ai_tool_call_log(使用 toolCall.nodeRef.dbId) |
RagCallRecordedEvent | recordRagCall | 异步 | 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 extractor11.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.13 | V0.0.13_ai_logging.sql | 初始三层表结构:ai_conversation_log / ai_node_execution_log / ai_llm_call_log |
| V0.0.14 | V0.0.14__refactor_logging_system.sql | 节点表加 state_before/state_after;拆分 LLM/Tool/RAG 三张调用表 |
| V0.0.15 | V0.0.15__create_streaming_content_log.sql | 新增 ai_streaming_content_log |
| V0.0.16 | V0.0.16__add_system_prompt_to_llm_call_log.sql | LLM 调用日志拆分 system_prompt / user_message |
| V0.0.25 | V0.0.25__refactor_logging_tree.sql | 节点表加 parent_node_execution_log_id / iteration / graph_name |
| V0.0.26 | V0.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 的上下文里,作为一条 L3ToolExecutionCall(SUB_AGENT)记录。没有独立的子 trace。 sendSubagentTask/sendSubagentMessage是另一套走 MQ dispatcher 的异步任务系统(AgentMessage.MessageType.TASK),仅被SubagentTaskTool使用,不在主 chat 路径上。这条路径下会产生独立子 trace。