链上 AI Agent 的决策可审计架构:事件溯源与不可篡改日志设计

一、Agent 决策链路不可追溯就等于黑箱

AI Agent 在链上执行操作——代币转移、合约调用、治理投票——会产生真实的资产和权限影响。如果 Agent 的决策链路不可追溯,出了问题只能看到"Agent 做了某件事",但无从得知"Agent 是在什么状态下做出这个决定的"。

一次看似异常的代币兑换,可能是正常策略执行(价格触及阈值),可能是 Oracle 数据延迟导致的误判,也可能是外部攻击者通过操纵链上数据误导了 Agent。没有完整决策链路的记录,系统将无法审计、无法复盘、无法问责。

可审计架构的核心诉求不是"记录一切",而是记录到能还原决策逻辑的程度。事件溯源(Event Sourcing)提供了天然合适的架构范式:不存储当前状态,而是存储状态变化的事件序列,当前状态是事件序列的回放结果。这和区块链的"交易序列 → 世界状态"模型完全吻合。

二、架构:事件溯源 + 链上日志 + 链下索引

flowchart TD
    A[Agent 决策请求] --> B[输入快照模块<br/>记录: 区块高度/价格/账户状态]
    B --> C[推理模块<br/>记录: 模型 ID/输入/输出/耗时]
    C --> D[决策模块<br/>记录: 决策动作/参数/置信度]
    D --> E[操作执行模块<br/>记录: 交易哈希/Gas/状态]
    
    B --> F[事件总线]
    C --> F
    D --> F
    E --> F
    
    F --> G[链上日志合约<br/>存储事件哈希 + 元数据]
    F --> H[链下索引<br/>存储完整事件详情]
    
    G --> I[链上可验证]
    H --> J[全文检索 + 可视化]

输入快照、推理、决策、执行——每个环节都产生事件。事件分为两类:

  • 链上事件:只存储哈希、时间戳、事件类型标识——数据量小,适合链上永久记录
  • 链下事件:存储完整的决策上下文、LLM 输入输出、模型参数——数据量大,存在索引层供检索

链上哈希保证"这个决策过程确实在某时某刻发生过"且不可篡改,链下数据保证"这个决策过程的细节可以被完整查看和审计"。两层一起构成不可否定的审计证据链。

三、生产级实现

链上审计日志合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * @title AgentAuditLog
 * @notice AI Agent 决策过程的链上审计日志
 * 
 * 设计决策:
 * - 只存哈希不存明文:链上存储贵,决策详情在链下索引
 * - 事件类型分级:INPUT_SNAPSHOT / REASONING / DECISION / EXECUTION
 * - 每个事件都有自增序号——保证顺序可追溯
 * - 不可删除、不可修改——写一次就是永久的审计记录
 */
contract AgentAuditLog {
    /// @notice 事件类型枚举
    enum EventType {
        INPUT_SNAPSHOT,   // 输入快照:记录了 Agent 看到的世界状态
        REASONING,        // 推理过程:模型信息、输出原文
        DECISION,         // 决策:选择了哪个动作、参数、置信度
        EXECUTION,        // 执行:链上交易哈希、Gas 消耗、状态变化
        EXCEPTION         // 异常:决策失败、执行回滚的原因
    }

    /// @notice 单条审计事件
    struct AuditEvent {
        uint256 id;               // 自增事件 ID(同一 Agent 内唯一)
        EventType eventType;      // 事件类型
        address agentAddress;     // Agent 合约地址
        bytes32 eventHash;        // 链下事件详情哈希(用于链下数据完整性验证)
        bytes32 parentHash;       // 上一事件的哈希——形成事件链
        uint256 blockNumber;      // 此时所在的区块高度
        uint256 timestamp;        // 区块时间戳
        bytes metadata;           // 压缩元数据(模型 ID + 版本 + 决策类型)
    }

    /// @notice 每个 Agent 的事件计数器
    mapping(address => uint256) public eventCounters;
    
    /// @notice 事件存储:agentAddress => eventId => AuditEvent
    mapping(address => mapping(uint256 => AuditEvent)) public events;

    event AuditEventRecorded(
        address indexed agent,
        uint256 indexed eventId,
        EventType eventType,
        bytes32 eventHash,
        bytes32 parentHash
    );

    /**
     * @notice 写入一条审计事件——只有 Agent 合约本身可以调用
     * @param eventType 事件类型
     * @param eventHash 链下事件详情的 keccak256 哈希
     * @param metadata 压缩元数据(编码格式由 Agent 自定义)
     * 
     * 设计决策:
     * - 限制调用者必须是 Agent 合约(通过白名单或接口检测)
     *   防止外部地址写入伪造的审计日志
     * - eventHash 连接链下详情:审计者可以用它验证链下数据是否被篡改
     */
    function recordEvent(
        EventType eventType,
        bytes32 eventHash,
        bytes calldata metadata
    ) external returns (uint256 eventId) {
        // 这里简化了调用者检查——生产环境需要维护 Agent 白名单
        // 或要求调用者实现 IAgent 接口
        
        eventId = ++eventCounters[msg.sender];
        
        bytes32 parentHash = eventId > 1 
            ? events[msg.sender][eventId - 1].eventHash 
            : bytes32(0);

        events[msg.sender][eventId] = AuditEvent({
            id: eventId,
            eventType: eventType,
            agentAddress: msg.sender,
            eventHash: eventHash,
            parentHash: parentHash,
            blockNumber: block.number,
            timestamp: block.timestamp,
            metadata: metadata
        });

        emit AuditEventRecorded(msg.sender, eventId, eventType, eventHash, parentHash);
    }

    /**
     * @notice 获取 Agent 的审计事件历史(分页)
     * @param agent Agent 地址
     * @param startId 起始事件 ID
     * @param count 返回数量上限(最多 100)
     * 
     * 分页是必需的——Agent 运行数月后可能有数万条事件
     */
    function getEventHistory(
        address agent,
        uint256 startId,
        uint256 count
    ) external view returns (AuditEvent[] memory result) {
        uint256 total = eventCounters[agent];
        uint256 end = startId + count;
        if (end > total + 1) end = total + 1;
        if (count > 100) count = 100;

        result = new AuditEvent[](end - startId);
        for (uint256 i = startId; i < end; i++) {
            result[i - startId] = events[agent][i];
        }
    }

    /**
     * @notice 验证链下数据完整性
     * @param eventId 事件 ID
     * @param offchainData 链下存储的完整事件数据
     * @return 数据哈希是否匹配链上记录
     */
    function verifyOffchainData(
        uint256 eventId,
        bytes calldata offchainData
    ) external view returns (bool) {
        AuditEvent storage evt = events[msg.sender][eventId];
        return keccak256(offchainData) == evt.eventHash;
    }
}

Agent 合约集成

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "./AgentAuditLog.sol";

/**
 * @notice 可审计的 AI Agent 基类
 * 
 * 设计决策:
 * - 决策和执行分离:先记录决策事件,再执行链上操作
 * - 即使执行失败,决策事件也会被记录——用于事后分析"为什么这个决策导致了失败"
 * - parentHash 链:每条事件引用上一条事件,形成不可篡改的事件链
 *   篡改中间任意一条,后续所有 parentHash 都会断裂
 */
abstract contract AuditableAgent {
    AgentAuditLog public immutable auditLog;

    constructor(AgentAuditLog _auditLog) {
        auditLog = _auditLog;
    }

    /**
     * @notice 执行一个完整的决策周期
     * @param snapshotHash 输入快照的哈希(链下存储完整快照)
     * @param reasoningHash 推理过程的哈希
     * @param decisionData 决策元数据(动作类型、参数摘要等)
     * 
     * 流程:输入快照 → 推理 → 决策 → 执行 → 记录
     * 任何步骤失败都会记录 EXCEPTION 事件
     */
    function executeDecisionCycle(
        bytes32 snapshotHash,
        bytes32 reasoningHash,
        bytes calldata decisionData
    ) external returns (bool) {
        // 步骤 1:记录输入快照
        auditLog.recordEvent(
            AgentAuditLog.EventType.INPUT_SNAPSHOT,
            snapshotHash,
            abi.encode(block.number, block.timestamp)
        );

        // 步骤 2:记录推理过程
        auditLog.recordEvent(
            AgentAuditLog.EventType.REASONING,
            reasoningHash,
            decisionData[:64] // 前 32 字节 = 模型 ID + 版本
        );

        // 步骤 3:执行决策(由子合约实现)
        try this._executeDecision(decisionData) returns (bytes32 txHash) {
            // 成功:记录执行事件
            auditLog.recordEvent(
                AgentAuditLog.EventType.EXECUTION,
                txHash,
                bytes("")
            );
            return true;
        } catch Error(string memory reason) {
            // 失败:记录异常事件,包含失败原因
            auditLog.recordEvent(
                AgentAuditLog.EventType.EXCEPTION,
                keccak256(bytes(reason)),
                bytes(reason)
            );
            return false;
        }
    }

    /// @notice 子合约实现具体决策逻辑
    function _executeDecision(bytes calldata decisionData) external virtual returns (bytes32 txHash);
}

链下索引服务:事件数据存储与检索

// lib/audit/OffchainEventStore.ts

interface OffchainEvent {
  eventId: string;              // "agentAddress-eventCounter"
  eventType: "input_snapshot" | "reasoning" | "decision" | "execution" | "exception";
  agentAddress: string;
  blockNumber: number;
  timestamp: number;
  
  // 详情(不同事件类型有不同字段)
  details: {
    // input_snapshot
    oraclePrices?: Record<string, number>;
    accountBalances?: Record<string, string>;
    
    // reasoning
    modelId?: string;
    modelVersion?: string;
    prompt?: string;
    response?: string;
    latencyMs?: number;
    
    // decision
    actionType?: string;
    params?: Record<string, any>;
    confidence?: number;
    
    // execution
    txHash?: string;
    gasUsed?: number;
    status?: string;
    
    // exception
    errorMessage?: string;
    errorStack?: string;
  };
}

/**
 * 链下事件存储——不可篡改的追加写
 * 
 * 设计决策:
 * - 使用 S3/Cloud Storage 的版本控制 + WORM(Write Once Read Many)
 *   确保存储层本身不可修改
 * - 每条事件独立存储:eventId 作为 key,方便按需检索
 * - 定时同步链上事件哈希:定期验证链下数据是否与链上一致
 */
class OffchainEventStore {
  private storage: any; // S3 / GCS / IPFS

  async saveEvent(event: OffchainEvent): Promise<string> {
    // 序列化并计算哈希——这个哈希必须和链上的 eventHash 一致
    const serialized = JSON.stringify(event, Object.keys(event).sort());
    const eventHash = ethers.keccak256(ethers.toUtf8Bytes(serialized));
    
    await this.storage.put(`${event.agentAddress}/${event.eventId}.json`, serialized);
    
    return eventHash;
  }

  async getEventHistory(
    agentAddress: string,
    fromId: number,
    toId: number
  ): Promise<OffchainEvent[]> {
    const events: OffchainEvent[] = [];
    for (let id = fromId; id <= toId; id++) {
      try {
        const raw = await this.storage.get(`${agentAddress}/${id}.json`);
        events.push(JSON.parse(raw));
      } catch {
        // 跳过不存在的记录——可能是索引未同步
        continue;
      }
    }
    return events;
  }

  /**
   * 完整性验证:对比链上 eventHash
   */
  async verifyIntegrity(
    agentAddress: string,
    eventId: number,
    auditLog: ethers.Contract
  ): Promise<boolean> {
    const raw = await this.storage.get(`${agentAddress}/${eventId}.json`);
    return auditLog.verifyOffchainData(eventId, ethers.toUtf8Bytes(raw));
  }
}

四、边界分析:日志完整 ≠ 问责完整

审计日志能保证的东西:

  • 决策过程的时序完整性:事件 A→B→C 的因果关系可追溯
  • 数据不可篡改:链上哈希 + 链下 WORM 存储,事后无法修改
  • 可审计性:第三方可以独立验证链上 eventHash 与链下数据的一致性

审计日志不能保证的东西:

  • Agent 的推理质量:日志只记录"Agent 看到了什么、输出了什么",不判断推理是否正确
  • Oracle 数据的原始可信度:如果 Oracle 提供了错误价格,日志如实记录了错误价格——这不代表 Agent 可以免责
  • 多 Agent 协作的因果关系:如果 Agent A 的决策影响了 Agent B,各自的日志独立存在,跨 Agent 的因果关系需要额外分析

数据增长问题:

一个活跃的 Agent 每天可能产生 100-10000 条事件。一年下来(按 1000 条/天)就是 36.5 万条。链上只存哈希没问题(gas 可控),但链下索引需要设计分层存储策略——热数据(近 7 天)在数据库,温数据(近 30 天)在对象存储,冷数据(> 30 天)归档到 Glacier 类存储。

隐私风险:

审计日志如果包含用户的链上地址、资产余额和决策偏好,构成了一个"行为画像"。被审计方(Agent 运营方)和被审计对象(用户)之间需要明确数据权限——用户应该可以查看与自己相关的日志,但不能查看其他用户的日志。这需要在存储层做按用户隔离,不能用统一的 plain JSON 文件。

五、总结

链上 AI Agent 的可审计架构基于事件溯源:将决策过程分解为输入快照、推理、决策、执行四个阶段的事件序列,每条事件同步写入链上合约(存哈希)和链下索引(存详情)。

链上 eventHash 保证不可篡改,parentHash 链接形成不可断裂的事件链。链下存储提供全文检索和决策回放能力。第三方可通过比对链上哈希和链下数据验证审计日志的完整性。数据增长需要分层存储策略,隐私需要按用户隔离。审计日志不能替代对推理质量、Oracle 可信度和多 Agent 协作的分析。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐