AI Agent 从 demo 进入生产,最容易翻车的地方通常不是模型本身,而是工具调用链。

一个 Agent 只要开始调用数据库、HTTP API、RAG 检索、工单系统或内部审批流,它就已经变成一个分布式系统。生产就绪的重点,是让每一次工具调用都有边界、有证据、可恢复、可审计。

本文记录一套我更常用的做法:先统一工具接口,再把 OpenTelemetry 埋到 Agent 编排层和工具执行层。

1. 工具调用先做统一契约

不要让每个工具各写各的参数、错误码和日志字段。否则一旦 Agent 调错工具,排查会非常痛苦。

建议最少固定这些字段:

  • tool_name:工具名称
  • tool_version:工具版本
  • trace_id:链路追踪 ID
  • input_schema_version:输入结构版本
  • timeout_ms:超时时间
  • retry_count:当前重试次数
  • error_code:统一错误码

一个极简接口可以这样写:

type ToolContext = {
  traceId: string;
  tenantId: string;
  userId?: string;
  timeoutMs: number;
};

type ToolResult<T> = {
  ok: boolean;
  data?: T;
  errorCode?: string;
  retryable?: boolean;
  evidence?: Record<string, unknown>;
};

interface AgentTool<I, O> {
  name: string;
  version: string;
  run(input: I, ctx: ToolContext): Promise<ToolResult<O>>;
}

这里的重点不是类型写得多漂亮,而是让所有工具都能被同一套执行器治理。

2. 执行器负责超时、重试和降级

不要把重试逻辑散落在每个工具里。工具只负责业务动作,执行器负责生产控制面。

async function executeTool<I, O>(
  tool: AgentTool<I, O>,
  input: I,
  ctx: ToolContext,
): Promise<ToolResult<O>> {
  const maxAttempts = 3;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const startedAt = Date.now();

    try {
      const result = await withTimeout(
        tool.run(input, ctx),
        ctx.timeoutMs,
      );

      return {
        ...result,
        evidence: {
          ...result.evidence,
          attempt,
          latencyMs: Date.now() - startedAt,
        },
      };
    } catch (error) {
      const retryable = isRetryableError(error);

      if (!retryable || attempt === maxAttempts) {
        return {
          ok: false,
          errorCode: normalizeError(error),
          retryable,
          evidence: {
            attempt,
            latencyMs: Date.now() - startedAt,
          },
        };
      }
    }
  }

  return { ok: false, errorCode: "UNKNOWN_TOOL_FAILURE" };
}

实际项目里还要加熔断、限流和幂等键。尤其是会写数据的工具,必须区分“可以重试”和“不能重试”。

3. OpenTelemetry 要埋在 Agent 决策边界

很多团队只记录 HTTP 请求日志,这对 Agent 不够。Agent 的关键链路是:用户输入、模型选择、工具选择、工具执行、结果合成。

每一步都应该有 span。

import { trace, SpanStatusCode } from "@opentelemetry/api";

const tracer = trace.getTracer("agent-runtime");

async function tracedToolCall<I, O>(
  tool: AgentTool<I, O>,
  input: I,
  ctx: ToolContext,
) {
  return tracer.startActiveSpan(`tool.${tool.name}`, async (span) => {
    span.setAttribute("tool.name", tool.name);
    span.setAttribute("tool.version", tool.version);
    span.setAttribute("tenant.id", ctx.tenantId);
    span.setAttribute("timeout.ms", ctx.timeoutMs);

    try {
      const result = await executeTool(tool, input, ctx);
      span.setAttribute("tool.ok", result.ok);

      if (!result.ok) {
        span.setAttribute("tool.error_code", result.errorCode ?? "unknown");
        span.setStatus({ code: SpanStatusCode.ERROR });
      }

      return result;
    } finally {
      span.end();
    }
  });
}

注意:不要把完整 prompt、个人信息、密钥或原始业务数据直接写进 span attribute。生产环境里通常只记录摘要、hash、版本号和必要的错误分类。

4. 审计日志和观测日志分开

可观测性是为了排障,审计是为了复盘和合规。两者不要混在一张日志表里。

观测日志关注:延迟、错误率、重试、token 成本、模型版本。

审计日志关注:谁触发了什么动作、Agent 为什么选择这个工具、工具返回了什么关键证据、最终是否影响业务状态。

如果是金融、工业 IoT 或内部审批系统,我会要求审计日志至少做到 append-only,并保留 trace_id,这样能从审计事件反查完整调用链。

5. 上线前检查清单

我通常会问这几件事:

  • 工具失败时,Agent 是重试、降级、转人工,还是继续编答案?
  • 每个工具有没有超时和统一错误码?
  • prompt、模型、工具 schema 是否有版本号?
  • OpenTelemetry 能不能看到一次请求里的模型调用和工具调用?
  • 审计日志能不能解释“为什么做了这个动作”?
  • 写操作有没有幂等键和回滚方案?

这些问题如果答不上来,Agent 还不适合直接接生产业务。

总结

生产级 AI Agent 不是“模型加几个工具”这么简单。

真正要补的是三条链:调用链、证据链、恢复链。工具调用框架负责把外部依赖管住,OpenTelemetry 负责让行为可见,审计日志负责让关键动作可追溯。

如果你的团队正在把 Agent 接进真实业务,建议先做一次生产就绪检查,把 P0/P1 风险列出来,再决定是继续扩功能,还是先补工程底座。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐