轻量 Agent 的日志与审计:结构化日志在 AI 行为追踪中的工程落地

📅 2026/7/12 19:06:15
轻量 Agent 的日志与审计:结构化日志在 AI 行为追踪中的工程落地
轻量 Agent 的日志与审计结构化日志在 AI 行为追踪中的工程落地一、Agent 自己编了一个工具调用然后执行了——AI 行为的不可追溯性AI Agent 在执行任务时产生的行为链——LLM 的每一次推理、工具的每一次调用、操作的每一次成功或失败——是理解 Agent 决策过程的核心依据。但在很多 Agent 项目中日志还停留在console.log(agent.step)的阶段。当用户问为什么 Agent 刚才调用了那个工具却给了这个回答时开发者只能靠猜测。传统应用的日志是线性的请求进来 → 处理 → 响应返回。Agent 的日志是多层嵌套的一个任务触发了 LLM 推理推理决定调用了工具 A工具 A 的结果被反馈给 LLM 做第二次推理第二次推理又调用了工具 B……这是一棵行为树而不是一条时间线。捕获这棵行为树的完整形态是 Agent 可观测性的基础。二、Agent 结构化日志的分层模型Run → Step → Spangraph TD A[Run: 一次用户任务] -- B1[Step 1: 初始 LLM 推理] A -- B2[Step 2: 工具调用 二次推理] A -- B3[Step 3: 最终输出] B1 -- C1[Span: prompt 构建br/tokens: 450] B1 -- C2[Span: API 调用br/latency: 1200ms] B1 -- C3[Span: 响应解析br/tool_calls: 2] B2 -- D1[Span: 工具 get_weatherbr/input: {city: 北京}] B2 -- D2[Span: 工具执行br/latency: 350ms] B2 -- D3[Span: 结果反馈 LLMbr/context_length: 3200] B3 -- E1[Span: 最终消息生成br/tokens: 180]每一层都有明确的含义Run一次完整的用户请求到最终响应的全过程Step一次 LLM 推理 工具调用循环的迭代Span一个原子操作——LLM API 调用、工具执行、prompt 构建三、结构化日志系统的完整实现// agent-logger/logger.ts — Agent 结构化日志系统 import { v4 as uuid } from uuid; // 日志级别 type LogLevel debug | info | warn | error; // Span 类型 type SpanType | llm_call | llm_parse | tool_call | tool_execute | prompt_build | context_compress | memory_retrieve | output_format; // 核心Span 数据结构 interface Span { id: string; parentId: string | null; // parent Step 或 Span 的 ID type: SpanType; name: string; startTime: number; endTime: number | null; // null 表示仍在进行中 status: running | success | error; metadata: Recordstring, unknown; // span 特定的元数据 error?: string; tags: string[]; // 用于过滤和聚合 } // Step一个推理—工具调用循环 interface Step { id: string; runId: string; index: number; // 第几步 input: string; // 该步的输入可能是上一步的输出 spans: Span[]; modelUsed: string; totalTokens: number; startTime: number; endTime: number | null; } // Run一次完整任务 interface Run { id: string; sessionId: string; userId: string; userMessage: string; agentType: string; steps: Step[]; status: running | completed | failed; startTime: number; endTime: number | null; metadata: Recordstring, unknown; } // 日志写入器接口 interface LogWriter { writeRun(run: Run): Promisevoid; writeStep(step: Step): Promisevoid; writeSpan(span: Span): Promisevoid; } // 控制台写入器——开发环境使用 class ConsoleWriter implements LogWriter { async writeRun(run: Run) { console.log(JSON.stringify({ _type: agent_run, timestamp: new Date().toISOString(), ...run, })); } async writeStep(step: Step) { console.log(JSON.stringify({ _type: agent_step, timestamp: new Date().toISOString(), ...step, })); } async writeSpan(span: Span) { if (span.status error) { console.error(JSON.stringify({ _type: agent_span, timestamp: new Date().toISOString(), ...span, })); } } } // Elasticsearch 写入器——生产环境使用 class ElasticsearchWriter implements LogWriter { constructor(private indexPrefix: string) {} async writeRun(run: Run) { // await es.index({ index: ${this.indexPrefix}-runs, body: run }); } async writeStep(step: Step) { // await es.index({ index: ${this.indexPrefix}-steps, body: step }); } async writeSpan(span: Span) { // await es.index({ index: ${this.indexPrefix}-spans, body: span }); } } // Agent 日志管理器——核心 API class AgentLogger { private currentRun: Run | null null; private currentStep: Step | null null; constructor( private writers: LogWriter[], ) {} // 开始一个新的 Run startRun(params: { sessionId: string; userId: string; userMessage: string; agentType: string; metadata?: Recordstring, unknown; }): Run { this.currentRun { id: uuid(), sessionId: params.sessionId, userId: params.userId, userMessage: params.userMessage, agentType: params.agentType, steps: [], status: running, startTime: Date.now(), endTime: null, metadata: params.metadata || {}, }; this.flushRun(); return this.currentRun; } // 开始一个新的 Step startStep(input: string, modelUsed: string): Step { if (!this.currentRun) { throw new Error(No active run. Call startRun() first.); } this.currentStep { id: uuid(), runId: this.currentRun.id, index: this.currentRun.steps.length, input, spans: [], modelUsed, totalTokens: 0, startTime: Date.now(), endTime: null, }; this.currentRun.steps.push(this.currentStep); return this.currentStep; } // 完成一个 Step endStep(output: string, totalTokens: number) { if (!this.currentStep) return; this.currentStep.endTime Date.now(); this.currentStep.totalTokens totalTokens; this.flushStep(); } // 创建一个 Span自动跟踪时间 async traceSpanT( type: SpanType, name: string, fn: (span: Span) PromiseT, ): PromiseT { const span: Span { id: uuid(), parentId: this.currentStep?.id ?? this.currentRun?.id ?? null, type, name, startTime: Date.now(), endTime: null, status: running, metadata: {}, tags: [], }; // 将 span 关联到当前 step this.currentStep?.spans.push(span); try { const result await fn(span); span.status success; span.endTime Date.now(); this.flushSpan(span); return result; } catch (error) { span.status error; span.endTime Date.now(); span.error String(error); this.flushSpan(span); throw error; } } // 完成一个 Run endRun(status: completed | failed completed) { if (!this.currentRun) return; this.currentRun.endTime Date.now(); this.currentRun.status status; this.flushRun(); } // 审计日志——记录安全相关事件 audit(event: string, details: Recordstring, unknown) { const auditEntry { timestamp: new Date().toISOString(), runId: this.currentRun?.id, stepId: this.currentStep?.id, event, details, }; // audit 日志写入专用索引不与其他日志混合 console.log(JSON.stringify({ _type: agent_audit, ...auditEntry })); } private flushRun() { if (!this.currentRun) return; for (const writer of this.writers) { writer.writeRun({ ...this.currentRun }).catch(console.error); } } private flushStep() { if (!this.currentStep) return; for (const writer of this.writers) { writer.writeStep({ ...this.currentStep }).catch(console.error); } } private flushSpan(span: Span) { for (const writer of this.writers) { writer.writeSpan({ ...span }).catch(console.error); } } }Agent 中集成日志系统// agent/runner.ts — Agent 运行器集成日志 class AgentRunner { constructor( private llm: LLMClient, private tools: ToolRegistry, private logger: AgentLogger, ) {} async run(userMessage: string, sessionId: string): Promisestring { const run this.logger.startRun({ sessionId, userId: /* from auth context */ user-123, userMessage, agentType: task-agent, }); try { let currentInput userMessage; const maxSteps 10; for (let i 0; i maxSteps; i) { const step this.logger.startStep(currentInput, gpt-4o); // LLM 调用 span const llmResponse await this.logger.traceSpan( llm_call, Step ${i 1} LLM inference, async (span) { span.metadata { model: gpt-4o, inputTokens: 450, temperature: 0.1, }; const response await this.llm.chat([ { role: system, content: You are a helpful assistant. }, { role: user, content: currentInput }, ]); span.metadata { ...span.metadata, outputTokens: response.usage?.output_tokens, latency: response.latency, }; return response; } ); const toolCalls llmResponse.tool_calls; if (!toolCalls || toolCalls.length 0) { const finalOutput llmResponse.content; this.logger.endStep(finalOutput, llmResponse.usage?.total_tokens || 0); this.logger.audit(agent_completed, { outputLength: finalOutput.length }); this.logger.endRun(completed); return finalOutput; } // 工具调用 spans for (const tc of toolCalls) { const toolResult await this.logger.traceSpan( tool_execute, Execute: ${tc.name}, async (span) { span.metadata { toolName: tc.name, arguments: tc.arguments }; span.tags [tc.name]; try { const result await this.tools.execute(tc.name, tc.arguments); span.metadata { ...span.metadata, success: true }; return result; } catch (error) { span.tags.push(error); this.logger.audit(tool_call_failed, { toolName: tc.name, error: String(error), }); throw error; } } ); // 将工具结果反馈给 LLM currentInput Tool ${tc.name} result: ${JSON.stringify(toolResult)}; } this.logger.endStep(currentInput, llmResponse.usage?.total_tokens || 0); } throw new Error(Max steps exceeded); } catch (error) { this.logger.audit(agent_failed, { error: String(error) }); this.logger.endRun(failed); throw error; } } }四、日志系统的边界Token 消耗、性能开销与隐私合规日志膨胀问题。一个 5 步的 Agent 运行可能产生 20 个 span每条 span 包含完整的 prompt 和响应。如果 prompt 包含大段的上下文如 RAG 的检索结果单次 Run 的日志量可能达到几十 KB。高并发下这会产生可观的存储和 I/O 成本。解决方案对 span 的metadata做截断——完整 prompt 保留在前 3 个 step 的 span 中后续 step 仅记录摘要。性能开销。每次traceSpan都会创建对象和 JSON 序列化。高并发 Agent 场景下日志写入应使用异步、批量、非阻塞的方式。ConsoleWriter 在开发环境 OK但在生产环境应替换为批量写入的 Elasticsearch/ClickHouse writer。隐私合规。Agent 的 prompt 和响应中可能包含用户的敏感信息。日志在写入存储前应过滤 PII个人身份信息。审计日志是一个特例——它应当记录完整的操作信息但不包含原始 prompt以平衡可追溯性和隐私保护。最后Agent 日志的价值体现在事后复盘和线上排查。当用户投诉 Agent 给出了错误答案时能够通过 Run ID 拉出完整的行为树逐 Step、逐 Span 地分析哪个环节出了偏差——这才是结构化日志的真正价值。五、总结Agent 的结构化日志应当采用 Run → Step → Span 的三级模型每一层记录不同的粒度的数据。通过traceSpan自动测量和记录每次操作的耗时和元数据日志系统在 Agent 代码中自然嵌入而非事后添加。落地时先实现 ConsoleWriter 确认日志结构合理再替换为持久化存储。审计日志独立于性能日志记录安全相关的关键事件。Agent 的日志不是为了监控面板——它是为了你能在Agent 做错了的时候追溯到错在哪一步、哪个工具调用、哪次 LLM 推理。在这些关键时刻一条清晰的 Span 比十个模糊的 console.log 有用得多。