欢迎光临
我们一直在努力

轻量 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 → Span

graph 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_weather<br/>input: {city: "北京"}]
B2 –> D2[Span: 工具执行<br/>latency: 350ms]
B2 –> D3[Span: 结果反馈 LLM<br/>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: Record<string, 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: Record<string, unknown>;
}

// 日志写入器接口
interface LogWriter {
writeRun(run: Run): Promise<void>;
writeStep(step: Step): Promise<void>;
writeSpan(span: Span): Promise<void>;
}

// 控制台写入器——开发环境使用
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?: Record<string, 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 traceSpan<T>(
type: SpanType,
name: string,
fn: (span: Span) => Promise<T>,
): Promise<T> {
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: Record<string, 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): Promise<string> {
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 有用得多。

赞(0)
未经允许不得转载:171主机测评 » 轻量 Agent 的日志与审计:结构化日志在 AI 行为追踪中的工程落地
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址