可观测性
运行中 Agent 的日志、审计与调试
当 Agent 出现异常时,必须弄清原因。Minara Agent 会产生三条独立的信息流:结构化日志、审计日志和层级事件。每条流都有其用途。本页说明各类信息的归属,以及如何排查常见故障。
为什么是三条流,而不是一个大日志? Agent 异常通常呈现三种形态:基础设施故障(LLM 调用失败、数据库锁死)、Agent 执行了错误操作(以错误参数调用工具),或权限逻辑意外拦截了某个操作。把三者混在一条流里会导致难以阅读。分开后,可以直接定位到对应的表:行为问题看
audit,权限问题看tier_events,基础设施问题看结构化日志。三条流共享trace_id,需要完整还原时可将它们串联起来。
实际示例:6 阶段资金安全会为每次涉及资金的调用向 tier_events 写入一行记录,以便事后证明某笔交易通过了所有预检。
三条信息流
| 信息流 | 存储位置 | 用途 |
|---|---|---|
| 结构化日志 | $dataDir/logs/*.ndjson 及标准输出 | 运行状态、启动信息、LLM 调用 |
| 审计日志 | SQLite audit 表 | "Agent 调用了哪些工具,结果如何" |
| 层级事件 | SQLite tier_events 表 | "该调用为何被允许或拦截" |
经验法则:调试行为问题,先看审计日志;调试基础设施问题,先看结构化日志;调试权限问题,先看层级事件。
结构化日志
apps/agent/src/core/logger.ts 提供了一个简洁的 JSON 日志记录器:
logger.info("skills/registry", "skill_registered", { id });
logger.warn("agent/loop", "max_iterations_reached", { session_id });
logger.error("llm/anthropic-wire", "cache_miss", { reason });每行是一个 JSON 对象,结构如下:
{
"ts": "2026-04-14T12:34:56.789Z",
"level": "info",
"category": "skills/registry",
"event": "skill_registered",
"correlation_id": "turn_abc123",
"trace_id": "t_xyz",
"data": { "id": "minara.core" }
}关联 ID 由包裹轮次执行的 withCorrelation(id, fn) 生成。轮次内产生的所有日志行通过 AsyncLocalStorage 继承相同的 ID。执行 grep correlation_id=turn_abc123 $dataDir/logs/*.ndjson 即可获取某轮次的完整记录。
日志轮转
日志按天轮转,写入 $dataDir/logs/。通过以下环境变量配置级别:
LOG_LEVEL(debug/info/warn/error,默认warn)
设置 LOG_LEVEL=debug 是安全的:结构化格式可用 jq 过滤噪音,但日志量约为原来的 4 倍。
审计日志:事实来源
所有工具调用均经过 apps/agent/src/core/audit-log-hook.ts 中的 auditLogHook。表结构如下:
CREATE TABLE audit (
id TEXT PRIMARY KEY,
session_id TEXT,
trace_id TEXT,
tool_name TEXT,
tool_set TEXT,
args_json TEXT, -- redacted
result_json TEXT,
blocked INTEGER,
block_reason TEXT,
permission_tier INTEGER,
source TEXT, -- user | cron | autopilot | delegation
duration_ms INTEGER,
created_at INTEGER
);
CREATE VIRTUAL TABLE audit_fts USING fts5(
tool_name, block_reason, args_json, content='audit'
);FTS5 支持对完整历史进行全文检索:
SELECT created_at, tool_name, blocked, block_reason
FROM audit
WHERE audit MATCH 'withdraw'
ORDER BY created_at DESC
LIMIT 50;排查时从这里开始。 当用户反馈"Agent 行为异常"时,第一步是按顺序拉取该会话的审计记录。LLM 的推理文本、工具参数、原始工具输出和时间戳都在其中。
脱敏处理
tools/_shared/result.ts 中的脱敏器会在数据写入审计日志前,将已知敏感字段(api_key、secret、password、token、private_key、mnemonic、seed)替换为 ***。加上密钥从不作为工具参数传入(而是在工厂函数阶段从 process.env 读取)的规则,审计日志可以放心分享给支持团队,或直接附在 bug 报告中,无需额外脱敏。
层级事件:拦截原因
core/permission-tier-hook.ts 对每次权限决策(无论允许还是拦截)都会向 tier_events 写入一行:
CREATE TABLE tier_events (
id TEXT PRIMARY KEY,
tool_name TEXT,
source TEXT,
tier INTEGER,
allow_ceiling INTEGER,
decision TEXT, -- allow | block | pending_confirmation
reason TEXT,
trace_id TEXT,
created_at INTEGER
);审计日志告诉你发生了什么;层级事件告诉你权限系统为何做出该决定。两张表通过 trace_id 关联,常一起查询:
SELECT a.tool_name, a.blocked, te.decision, te.reason
FROM audit a
LEFT JOIN tier_events te ON te.trace_id = a.trace_id
AND te.tool_name = a.tool_name
WHERE a.session_id = ?
ORDER BY a.created_at;关联 ID 与追踪 ID
系统中有两种不同的 ID:
correlation_id以轮次为粒度,在 Agent 循环每轮开始时生成,出现在结构化日志和审计记录中。trace_id以工作流运行或信号为粒度,从SignalContext或WorkflowInstance传递到下游所有工具调用,包括子 Agent 委托。
用户聊天轮次通常只有新的 correlation_id,没有 trace_id。定时任务触发时两者都有。trace_id 可用于查询"14:03 的 BTC 提醒在整个生命周期内做了什么,包括它派生的子 Agent"。
常见排查场景
"Agent 为何拒绝调用 X?"
SELECT tool_name, decision, reason, created_at
FROM tier_events
WHERE tool_name = 'swap'
AND decision = 'block'
ORDER BY created_at DESC
LIMIT 10;查看 reason 字段,常见值如下:
tier_exceeds_ceiling:技能未激活,或当前轮次的allowRiskTier低于工具的风险等级。analysis_to_trade_boundary:该轮次已进行了分析调用;交易调用必须在独立的用户消息中发起。daily_cap_exceeded:daily_spend加上本次调用的名义金额将超过MINARA_DAILY_CAP_USD。kill_switch_active:某人(或 Agent 本身)触发了kill。tool_set_not_allowed:当前轮次的allowedToolSets不包含该工具。
"为什么这次轮次很慢?"
SELECT tool_name, AVG(duration_ms) AS avg_ms, COUNT(*) AS n
FROM audit
WHERE session_id = ?
GROUP BY tool_name
ORDER BY avg_ms DESC;结合按 correlation_id 过滤的结构化日志,可查看 LLM 调用耗时和缓存命中率。
"提示词缓存是否生效?"
在日志中检索 llm.cache_read_input_tokens。如果某会话的该值全为 0,说明可缓存的提示词块不稳定(可能每轮都在重新生成,导致缓存失效)。参见 LLM 集成。
"Autopilot 昨夜做了什么?"
SELECT created_at, tool_name, blocked, substr(result_json, 1, 200)
FROM audit
WHERE source = 'autopilot'
AND created_at > ?
ORDER BY created_at;健康检查接口
HTTP 网关提供以下接口:
GET /healthz:存活探针,进程正常且数据库可打开时返回200。GET /status:就绪状态详情,包含数据库统计、技能数量、活跃触发器、最近一次 LLM 调用成功时间。
完整 schema 参见 API 参考。
对接外部工具
如需将日志接入 Loki / Datadog / Grafana Cloud,可通过管道传输标准输出:
docker run minara 2>&1 | vector --config vector.toml所有标准输出均为合法的 NDJSON,没有单独的"结构化日志"导出路径,标准输出即为规范来源。
可观测性的设计有意保持简单:三张表、一种日志格式、一个关联字段、一个追踪字段。出现问题时,读行记录;一切正常时,忽略它。这就是全部的设计。