Skip to content

审计与脱敏:出事能追溯,记录不泄密 ​

1. 本节产出 ​

一套审计系统:每个 Agent 运行的完整轨迹可回放(含每一轮的 Thought/Action/Observation、Token、耗时、审批记录),敏感信息在落库前脱敏,并且能回答「这个操作是谁让 AI 做的」。

2. 前置依赖 ​

3. 为什么 Agent 的审计比普通系统难 ​

普通系统的审计:谁、什么时间、调了哪个接口、参数是什么。够用。

Agent 的审计要回答的问题更难:

问题为什么难
AI 为什么做这个操作?需要 Thought 和上下文
是哪个输入导致它这么做的?可能是文档内容、工具返回值、用户话术中的任何一处
审批是谁批的?需要关联审批记录
花了多少钱?需要按运行聚合 Token

核心难点:要能「回放」整个决策过程。 只记录最终动作是不够的,出事时你需要知道「它当时看到了什么、想到了什么」。

另一个矛盾:审计要记录详细内容,但详细内容里往往包含敏感信息(用户的手机号、订单金额、内部系统地址)。记录得越全,泄密风险越大。 这就是脱敏要解决的问题。

4. 核心原理 ​

4.1 审计要记录什么 ​

一次 Agent 运行的审计记录:
  ├─ 基本信息:runId、租户、用户、目标、起止时间、状态
  ├─ 每一轮:Thought、Action、工具名、参数、Observation、耗时、Token
  ├─ 工具调用:每次调用的工具、参数、结果摘要、是否审批
  ├─ 审批记录:谁批的、何时、理由
  ├─ 安全事件:越权尝试、编造工具名等
  └─ 成本:总 Token、各轮分布、估算金额

4.2 脱敏策略 ​

信息类型处理方式
手机号138****1234
身份证3301**********1234
银行卡只留后 4 位
邮箱a***@example.com
API Key / 密钥完全移除,不记录
地址保留到区县级
姓名视合规要求,可保留或脱敏

密钥必须完全移除而不是打码:打码后的密钥仍有泄露风险(部分前缀可能可猜)。

4.3 脱敏的时机 ​

关键:脱敏必须在「落库前」,不是「查询时」

  记录 → 脱敏 → 落库 ──→ 查询时无需处理
  
  错误做法:记录 → 落库(明文)→ 查询时脱敏
     问题:库里是明文的,任何有库权限的人都能看到

落库前脱敏是唯一正确的做法。查询时脱敏只是「看起来安全」,实际上数据库里全是明文。

但要注意:脱敏后就无法回放完整的原始过程了。折中方案:

方案做法
全脱敏简单,但排查时信息不足
分级保留敏感字段脱敏,其余保留;原始内容加密存储 + 严格权限 + 短期保留

推荐分级保留:脱敏后的记录用于日常查询,加密的原始记录用于事故调查(且有访问审计)。

4.4 保留期限与合规 ​

审计日志保留期:按行业要求(金融通常 5 年以上)
原始内容(含敏感信息):短期(如 30 天)后自动删除
访问审计记录本身:也要留痕(谁查了审计日志)

5. 代码走查 ​

5.1 审计模型 ​

sql
CREATE TABLE agent_audit (
  id            BIGSERIAL PRIMARY KEY,
  run_id        VARCHAR(64) NOT NULL,
  tenant_id     VARCHAR(64) NOT NULL,
  user_id       VARCHAR(64),
  iteration     INT,
  thought       TEXT,          -- 已脱敏
  tool_name     VARCHAR(100),
  tool_args     JSONB,         -- 已脱敏
  observation   TEXT,          -- 已脱敏
  tokens        INT,
  duration_ms   BIGINT,
  at            TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_audit_run ON agent_audit (run_id, iteration);
CREATE INDEX idx_audit_tenant ON agent_audit (tenant_id, at);

-- 审计日志的访问也要留痕
CREATE TABLE audit_access_log (
  id         BIGSERIAL PRIMARY KEY,
  accessor   VARCHAR(64),
  run_id     VARCHAR(64),
  reason     TEXT,
  at         TIMESTAMPTZ DEFAULT now()
);

5.2 脱敏器 ​

java
// ch03-agent/src/main/java/com/aitech/agent/audit/Masker.java
@Component
public class Masker {

    private static final Pattern PHONE =
            Pattern.compile("(1[3-9]\\d)\\d{4}(\\d{4})");
    private static final Pattern ID_CARD =
            Pattern.compile("(\\d{4})\\d{10}(\\w{4})");
    private static final Pattern EMAIL =
            Pattern.compile("(\\w)[\\w.-]*(@[\\w.-]+)");
    private static final Pattern KEY =
            Pattern.compile("(sk-|AKID|Bearer\\s+)[A-Za-z0-9_\\-]{8,}");

    public String mask(String text) {
        if (text == null) return null;
        String r = PHONE.matcher(text).replaceAll("$1****$2");
        r = ID_CARD.matcher(r).replaceAll("$1**********$2");
        r = EMAIL.matcher(r).replaceAll("$1***$2");
        r = KEY.matcher(r).replaceAll("[KEY_REMOVED]");   // 完全移除
        return r;
    }

    public Map<String, Object> maskArgs(Map<String, Object> args) {
        return args.entrySet().stream().collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> e.getValue() instanceof String s ? mask(s) : e.getValue()));
    }
}

5.3 审计 Advisor ​

java
// ch03-agent/src/main/java/com/aitech/agent/audit/AuditAdvisor.java
@Component
public class AuditAdvisor implements CallAroundAdvisor {

    private final Masker masker;
    private final AuditRepository repo;

    @Override
    public AdvisedResponse aroundCall(AdvisedRequest req, CallAroundAdvisorChain chain) {
        long t0 = System.nanoTime();
        AdvisedResponse resp;

        try {
            resp = chain.nextAroundCall(req);
        } catch (Exception e) {
            // 失败也要审计——「谁在什么时候发起了一个失败的请求」同样重要
            repo.save(AuditRecord.failed(req, masker, e));
            throw e;
        }

        repo.save(AuditRecord.of(req, resp, masker, elapsed(t0)));
        return resp;
    }

    @Override public String getName() { return "audit"; }
    @Override public int getOrder() { return 100; }      // 最前:拿到原始输入
}

失败也要记录是重要细节:只记录成功的调用,会让攻击尝试、异常行为从审计里消失——而这些恰恰是最需要审计的。

5.4 轨迹回放接口 ​

java
// ch03-agent/src/main/java/com/aitech/agent/controller/AuditController.java
@RestController
@RequestMapping("/api/audit")
public class AuditController {

    @GetMapping("/{runId}")
    @PreAuthorize("hasRole('AUDITOR')")
    public List<AuditRecord> trace(@PathVariable String runId,
                                    @RequestParam String reason,
                                    Authentication auth) {
        // 访问审计本身也留痕
        accessLog.record(auth.getName(), runId, reason);

        return repo.findByRunId(runId);
    }

    /** 回答「这个操作是谁让 AI 做的」 */
    @GetMapping("/{runId}/who")
    @PreAuthorize("hasRole('AUDITOR')")
    public Responsibility responsibility(@PathVariable String runId) {
        AgentRun run = runRepo.findById(runId).orElseThrow();
        List<ApprovalAudit> approvals = approvalRepo.findByRunId(runId);

        return new Responsibility(
                run.userId(),                    // 发起人
                run.goal(),                      // 原始指令
                approvals,                       // 谁批准了哪些操作
                run.createdAt(), run.updatedAt());
    }
}

6. 跑起来 ​

bash
git checkout ch03-12-audit
mvn spring-boot:run
bash
# 1. 跑一个含敏感信息的任务
curl -X POST http://localhost:8080/api/agent/run \
  -d '{"goal":"查询客户张三(13812345678)的订单并修改状态"}'

# 2. 查看审计轨迹(脱敏后)
curl -H "X-User: auditor" "http://localhost:8080/api/audit/{runId}?reason=排查"
# 期望:手机号显示为 138****5678,完整轨迹可见

# 3. 直接查数据库验证(关键)
psql -c "SELECT thought, tool_args FROM agent_audit LIMIT 5"
# 期望:库中也是脱敏的(证明是落库前脱敏,不是查询时脱敏)

# 4. 责任追溯
curl "http://localhost:8080/api/audit/{runId}/who"
# 期望:发起人、原始指令、审批人、时间

# 5. 访问留痕
psql -c "SELECT * FROM audit_access_log"
# 期望:审计员的访问被记录
检查项通过标准
轨迹完整每一轮的 Thought/Action/Observation 都在
落库即脱敏直接查库也是脱敏的
密钥移除Key 类信息完全不出现
失败也记录失败的调用同样有审计
责任可追溯能回答发起人、审批人
访问留痕查审计的人被记录

第三项「直接查库验证」是本节的关键验收:它证明脱敏发生在落库前。

7. 生产避坑 ​

  1. 脱敏必须在落库前,不能在查询时。查询时脱敏只是展示层的安全,数据库里仍是明文,任何有库权限的人(DBA、运维、备份文件)都能看到完整敏感信息。这是合规检查必查项。
  2. 失败的调用也要审计。只记录成功调用会让异常行为从审计中消失——而越权尝试、注入攻击恰恰是「失败的调用」。审计的价值在于发现异常,不在于统计成功。
  3. 访问审计日志本身要留痕并限权。审计日志包含大量业务信息,是内部人员泄露的高风险点。做法:访问需要专门角色、必须填写理由、访问行为本身被记录。

8. 延伸与锚点 ​

  • 思考题:超时、重试、熔断、循环检测、权限、审批、审计——这七件事怎么组织成一个可复用的组件?(03B 收官——答案在下一课时)
  • 代码锚点:git checkout ch03-12-audit
  • 下一课时:03-13 Harness 机制总装
  • 对应课件:L03-12 审计与脱敏