Agent 做的每一个操作都应该有记录——不是为了监控,而是为了可追溯、可复盘、可合规。本文设计一套完整的审计日志系统,记录 Agent 的工具调用、Prompt 内容、代码变更 diff、审批记录和成本消耗,让每一次 Agent 操作都可以回放和审查。

Agent 审计日志规范:工具调用、Prompt、diff 与审批记录

Agent 做的每一个操作都应该有记录——不是为了监控,而是为了可追溯、可复盘、可合规。本文设计一套完整的审计日志系统,记录 Agent 的工具调用、Prompt 内容、代码变更 diff、审批记录和成本消耗,让每一次 Agent 操作都可以回放和审查。

一、审计日志的价值

场景 没有审计日志 有审计日志
事故复盘 "不知道 Agent 做了什么" 可以回放完整操作序列
合规审查 "无法证明 Agent 没有访问敏感数据" 日志证明权限边界
成本分析 "不知道钱花在哪了" 精确到每次调用的成本
质量改进 "不知道 Agent 为什么犯错" 分析 Prompt 和决策过程
权限优化 "不知道 Agent 实际需要哪些权限" 基于实际使用调整权限

二、审计日志数据模型

2.1 核心事件表

sql
-- Agent 审计日志主表
CREATE TABLE agent_audit_logs (
    id              BIGSERIAL PRIMARY KEY,
    event_time      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    
    -- 会话信息
    session_id      VARCHAR(64) NOT NULL,
    task_id         VARCHAR(64),
    
    -- 操作者信息
    user_id         VARCHAR(64) NOT NULL,
    user_name       VARCHAR(128),
    agent_type      VARCHAR(32),           -- claude-code / codex / cursor / ...
    model_id        VARCHAR(64),
    
    -- 事件类型
    event_type      VARCHAR(32) NOT NULL,  -- tool_call / prompt / diff / approval / cost
    event_action    VARCHAR(64),           -- read_file / write_file / run_command / ...
    
    -- 事件内容(JSON)
    payload         JSONB NOT NULL,
    
    -- 元信息
    risk_level      VARCHAR(16),           -- LOW / MEDIUM / HIGH / CRITICAL
    approval_id     VARCHAR(64),           -- 关联的审批记录
    cost_usd        DECIMAL(10,6),
    tokens_used     INTEGER,
    
    -- 索引字段
    project_id      VARCHAR(64),
    repo_name       VARCHAR(256),
    
    -- 完整性
    checksum        VARCHAR(64)            -- SHA-256 校验和(防篡改)
);

-- 索引
CREATE INDEX idx_audit_session ON agent_audit_logs (session_id, event_time);
CREATE INDEX idx_audit_user ON agent_audit_logs (user_id, event_time);
CREATE INDEX idx_audit_task ON agent_audit_logs (task_id, event_time);
CREATE INDEX idx_audit_event_type ON agent_audit_logs (event_type, event_time);
CREATE INDEX idx_audit_risk ON agent_audit_logs (risk_level, event_time);

2.2 事件类型定义

yaml
# audit-event-types.yaml
event_types:
  
  # 工具调用事件
  tool_call:
    description: "Agent 调用了某个工具"
    actions:
      - read_file          # 读取文件
      - write_file         # 写入文件
      - run_command        # 执行命令
      - search_code        # 搜索代码
      - create_pr          # 创建 PR
      - merge_pr           # 合并 PR
    payload_schema:
      tool_name: string
      parameters: object
      result_summary: string
      duration_ms: integer
      success: boolean
    
  # Prompt 事件
  prompt:
    description: "Agent 接收或生成了 Prompt"
    actions:
      - prompt_received    # 接收用户 Prompt
      - prompt_generated   # Agent 生成 Prompt
      - context_loaded     # 加载上下文包
    payload_schema:
      prompt_text: string          # 脱敏后的 Prompt
      token_count: integer
      context_files: list[string]
      has_sensitive_data: boolean
  
  # 代码变更事件
  diff:
    description: "Agent 产生了代码变更"
    actions:
      - file_created       # 创建文件
      - file_modified      # 修改文件
      - file_deleted       # 删除文件
    payload_schema:
      file_path: string
      change_type: string          # created / modified / deleted
      lines_added: integer
      lines_removed: integer
      diff_content: string         # 脱敏后的 diff
      affected_modules: list[string]
  
  # 审批事件
  approval:
    description: "操作需要或获得了审批"
    actions:
      - approval_requested  # 请求审批
      - approval_granted    # 审批通过
      - approval_denied     # 审批拒绝
      - approval_timeout    # 审批超时
    payload_schema:
      approval_id: string
      requester: string
      approver: string
      reason: string
      risk_level: string
      expires_at: timestamp
  
  # 成本事件
  cost:
    description: "API 调用产生的成本"
    actions:
      - api_call           # API 调用
    payload_schema:
      model: string
      input_tokens: integer
      output_tokens: integer
      cost_usd: decimal
      cache_hit: boolean

2.3 日志记录器

python
# app/audit/logger.py
"""
Agent 审计日志记录器。
记录所有 Agent 操作到数据库。
"""
import json
import hashlib
from datetime import datetime
from typing import Optional

class AuditLogger:
    def __init__(self, db_connection):
        self.db = db_connection
    
    def log_tool_call(self, session_id: str, task_id: str, user_id: str,
                      tool_name: str, parameters: dict, result_summary: str,
                      duration_ms: int, success: bool, risk_level: str = "LOW"):
        """记录工具调用"""
        
        # 脱敏处理
        safe_params = self._sanitize_parameters(tool_name, parameters)
        
        payload = {
            "tool_name": tool_name,
            "parameters": safe_params,
            "result_summary": result_summary[:500],  # 截断
            "duration_ms": duration_ms,
            "success": success,
        }
        
        self._insert_event(
            session_id=session_id,
            task_id=task_id,
            user_id=user_id,
            event_type="tool_call",
            event_action=tool_name,
            payload=payload,
            risk_level=risk_level,
        )
    
    def log_prompt(self, session_id: str, user_id: str, prompt_text: str,
                   token_count: int, context_files: list, is_user_prompt: bool):
        """记录 Prompt 事件"""
        
        # 脱敏:移除敏感信息
        sanitized_prompt = self._sanitize_prompt(prompt_text)
        
        payload = {
            "prompt_text": sanitized_prompt,
            "token_count": token_count,
            "context_files": context_files,
            "has_sensitive_data": sanitized_prompt != prompt_text,
            "is_user_prompt": is_user_prompt,
        }
        
        self._insert_event(
            session_id=session_id,
            user_id=user_id,
            event_type="prompt",
            event_action="prompt_received" if is_user_prompt else "prompt_generated",
            payload=payload,
            risk_level="LOW",
        )
    
    def log_diff(self, session_id: str, task_id: str, user_id: str,
                 file_path: str, change_type: str, diff_content: str,
                 lines_added: int, lines_removed: int):
        """记录代码变更"""
        
        # 脱敏:移除密钥、密码等
        sanitized_diff = self._sanitize_diff(diff_content)
        
        payload = {
            "file_path": file_path,
            "change_type": change_type,
            "lines_added": lines_added,
            "lines_removed": lines_removed,
            "diff_content": sanitized_diff,
        }
        
        # 根据文件路径判断风险等级
        risk = self._assess_diff_risk(file_path)
        
        self._insert_event(
            session_id=session_id,
            task_id=task_id,
            user_id=user_id,
            event_type="diff",
            event_action=f"file_{change_type}",
            payload=payload,
            risk_level=risk,
        )
    
    def log_approval(self, session_id: str, user_id: str, approval_id: str,
                     action: str, approver: str, reason: str, risk_level: str):
        """记录审批事件"""
        
        payload = {
            "approval_id": approval_id,
            "requester": user_id,
            "approver": approver,
            "reason": reason,
            "risk_level": risk_level,
        }
        
        self._insert_event(
            session_id=session_id,
            user_id=user_id,
            event_type="approval",
            event_action=action,
            payload=payload,
            risk_level=risk_level,
        )
    
    def log_cost(self, session_id: str, user_id: str, model: str,
                 input_tokens: int, output_tokens: int, cost_usd: float):
        """记录成本事件"""
        
        payload = {
            "model": model,
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
            "cost_usd": cost_usd,
        }
        
        self._insert_event(
            session_id=session_id,
            user_id=user_id,
            event_type="cost",
            event_action="api_call",
            payload=payload,
            cost_usd=cost_usd,
            tokens_used=input_tokens + output_tokens,
        )
    
    def _insert_event(self, session_id: str, task_id: Optional[str],
                      user_id: str, event_type: str, event_action: str,
                      payload: dict, risk_level: str,
                      cost_usd: float = None, tokens_used: int = None):
        """插入事件到数据库"""
        
        # 计算校验和(防篡改)
        payload_json = json.dumps(payload, sort_keys=True)
        checksum = hashlib.sha256(payload_json.encode()).hexdigest()
        
        self.db.execute("""
            INSERT INTO agent_audit_logs 
            (session_id, task_id, user_id, event_type, event_action, 
             payload, risk_level, cost_usd, tokens_used, checksum)
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
        """, (session_id, task_id, user_id, event_type, event_action,
              payload_json, risk_level, cost_usd, tokens_used, checksum))
    
    def _sanitize_parameters(self, tool_name: str, params: dict) -> dict:
        """脱敏工具参数"""
        safe = params.copy()
        
        # 移除敏感字段
        sensitive_keys = ["password", "secret", "token", "api_key", "private_key"]
        for key in sensitive_keys:
            if key in safe:
                safe[key] = "***REDACTED***"
        
        return safe
    
    def _sanitize_prompt(self, prompt: str) -> str:
        """脱敏 Prompt 内容"""
        import re
        # 移除 API Key 模式
        prompt = re.sub(r"sk-[a-zA-Z0-9]{32,}", "***API_KEY***", prompt)
        # 移除密码模式
        prompt = re.sub(r"password[:=]\s*\S+", "password=***", prompt)
        return prompt
    
    def _sanitize_diff(self, diff: str) -> str:
        """脱敏 diff 内容"""
        # 移除密钥、Token 等
        import re
        diff = re.sub(r"[\"'](?:sk-|pk-|ghp_)[a-zA-Z0-9]{32,}[\"']", '"***SECRET***"', diff)
        return diff
    
    def _assess_diff_risk(self, file_path: str) -> str:
        """根据文件路径评估风险等级"""
        high_risk_patterns = [
            "payment", "auth", "security", "crypto",
            "migrations", "schema", "config/prod",
        ]
        
        for pattern in high_risk_patterns:
            if pattern in file_path.lower():
                return "HIGH"
        
        medium_risk_patterns = ["controller", "service", "handler"]
        for pattern in medium_risk_patterns:
            if pattern in file_path.lower():
                return "MEDIUM"
        
        return "LOW"

三、日志查询与回放

3.1 会话回放查询

sql
-- 回放某个会话的完整操作序列
SELECT 
    event_time,
    event_type,
    event_action,
    payload->>'tool_name' as tool_name,
    payload->>'file_path' as file_path,
    risk_level,
    cost_usd
FROM agent_audit_logs
WHERE session_id = 'session-2024-06-15-001'
ORDER BY event_time;

3.2 高风险操作查询

sql
-- 查询所有高风险操作
SELECT 
    event_time,
    user_name,
    event_action,
    payload->>'file_path' as file_path,
    payload->>'reason' as reason
FROM agent_audit_logs
WHERE risk_level IN ('HIGH', 'CRITICAL')
  AND event_time >= NOW() - INTERVAL '7 days'
ORDER BY event_time DESC;

3.3 成本分析查询

sql
-- 按用户统计成本
SELECT 
    user_name,
    COUNT(*) as call_count,
    SUM(cost_usd) as total_cost,
    SUM(tokens_used) as total_tokens,
    AVG(cost_usd) as avg_cost_per_call
FROM agent_audit_logs
WHERE event_type = 'cost'
  AND event_time >= NOW() - INTERVAL '30 days'
GROUP BY user_name
ORDER BY total_cost DESC;

四、日志导出与合规

4.1 合规导出

python
# app/audit/compliance_export.py
"""
审计日志合规导出。
支持导出为 CSV、JSON、PDF 格式。
"""
import csv
import json
from datetime import date, timedelta

def export_audit_logs(start_date: date, end_date: date, 
                      format: str = "csv", output_path: str):
    """导出审计日志"""
    
    logs = db.query("""
        SELECT * FROM agent_audit_logs
        WHERE event_time BETWEEN %s AND %s
        ORDER BY event_time
    """, (start_date, end_date))
    
    if format == "csv":
        with open(output_path, 'w', newline='') as f:
            writer = csv.DictWriter(f, fieldnames=logs[0].keys())
            writer.writeheader()
            writer.writerows(logs)
    
    elif format == "json":
        with open(output_path, 'w') as f:
            json.dump(logs, f, indent=2, default=str)
    
    elif format == "pdf":
        # 生成 PDF 报告
        generate_pdf_report(logs, start_date, end_date, output_path)
    
    # 生成导出记录
    log_export_event(start_date, end_date, format, output_path)

4.2 日志保留策略

yaml
# audit-retention-policy.yaml
retention:
  # 热存储(快速查询)
  hot_storage:
    duration_days: 90
    storage: "PostgreSQL"
    purpose: "日常查询、会话回放"
  
  # 温存储(归档查询)
  warm_storage:
    duration_days: 365
    storage: "S3 + Athena"
    purpose: "合规审查、事故复盘"
  
  # 冷存储(长期归档)
  cold_storage:
    duration_days: 2555  # 7 年
    storage: "S3 Glacier"
    purpose: "法律合规要求"
  
  # 清理策略
  cleanup:
    schedule: "daily"
    action: "move_to_next_tier"
    notification: true

五、真实经验与踩坑

5.1 日志量太大导致性能问题

场景:记录了每次工具调用的完整参数和结果,一天产生 50GB 日志。 问题:数据库查询变慢,存储成本飙升。 解决方案

  • 日志内容截断(参数和结果只保留前 500 字符)
  • 大文件 diff 只记录统计信息(行数),不记录完整内容
  • 超过 30 天的日志归档到 S3,数据库只保留热数据

5.2 敏感信息泄露到日志

场景:Agent 在执行任务时读取了包含 API Key 的配置文件,完整内容被记录到日志。 问题:日志被导出后,API Key 泄露。 解决方案

  • 日志记录器增加脱敏层(正则匹配常见的密钥模式)
  • 定期扫描日志,检测是否有漏网的敏感信息
  • 日志存储加密,访问需要额外权限

5.3 日志校验和没有被验证

场景:记录了每条日志的 SHA-256 校验和,但从来没有验证过。 问题:有人篡改了日志(删除了某条高风险操作记录),但没被发现。 解决方案

  • 每小时运行一次校验和验证任务
  • 校验失败的日志立即告警
  • 日志写入后不可修改(只追加,不更新/删除)

六、参数说明表

参数 类型 默认值 说明
log_level string "INFO" 日志级别
max_payload_size int 500 日志内容最大字符数
sensitive_patterns list 内置 敏感信息正则模式
retention_hot_days int 90 热存储保留天数
retention_warm_days int 365 温存储保留天数
retention_cold_days int 2555 冷存储保留天数(7 年)
checksum_enabled bool true 是否启用校验和
export_formats list ["csv", "json"] 支持的导出格式
alert_on_high_risk bool true 高风险操作是否告警
anonymize_pii bool true 是否匿名化个人信息

七、落地检查清单

  • 所有 Agent 操作都记录到审计日志
  • 日志包含完整的上下文(session_id、task_id、user_id)
  • 敏感信息已脱敏(API Key、密码、个人信息)
  • 日志内容有大小限制(防止日志爆炸)
  • 高风险操作有实时告警
  • 日志校验和已启用且定期验证
  • 日志保留策略符合合规要求
  • 日志可以按会话、任务、用户查询
  • 日志导出功能已测试
  • 日志存储加密,访问有权限控制

八、系列导航

上一篇:Agent 团队使用规范:开发者、Reviewer、Tech Lead 如何分工 下一篇:Agent 隐私与数据边界:什么代码不能进模型上下文