阅读视图

发现新文章,点击刷新页面。

前端开发者做 Agent:别写成一次请求,用 5 步受控循环防止 AI 乱跑

作者:前端转 AI 深度实践者

【省流助手/核心观点】:Agent 不是“调用一次大模型”就结束,也不是让模型无限自由发挥。一个最小 Agent Loop 至少要经历 5 步:接收用户输入、模型决定是否调用工具、程序执行工具、工具结果写回上下文、模型生成最终回答。真正可靠的 Agent Loop 必须受控:有 maxSteps、有工具边界、有结构化输出、有错误处理、有 trace 记录。对前端开发者来说,它很像一个带状态、带副作用、带退出条件的事件循环。


很多人第一次做 Agent,会把它想得过于神秘。

仿佛只要 Prompt 写成:

你是一个自主智能体,请一步一步完成用户任务。

模型就会自己查资料、自己调接口、自己整理结论、自己处理异常。

这个想象很美,但工程系统不能靠想象跑。

真正的 Agent,不是让模型无限自由发挥。
真正的 Agent,是让模型和工具在程序控制下协作。

这一层协作,叫 Agent Loop

1. 痛点:一次调用只能聊天,不能稳定办事

前两篇我们讲了 Tool Calling 和工具 Schema。

模型不应该编真实世界的信息,而应该提出工具调用意图。

比如用户问:

帮我查一下订单 A1001 到哪了,并告诉我什么时候能收到。

模型可以输出:

{
  "type": "tool_call",
  "toolName": "getOrderStatus",
  "args": {
    "orderId": "A1001"
  }
}

程序执行工具,得到:

{
  "status": "shipping",
  "eta": "2026-05-03"
}

问题来了:这就结束了吗?

对程序来说,工具结果已经拿到了。
但对用户来说,他想看的不是 JSON,而是一句能读懂的话:

订单 A1001 当前运输中,预计 2026-05-03 送达。

所以完整流程应该是:

用户提问
-> 模型提出工具调用
-> 程序执行工具
-> 工具结果写回上下文
-> 模型组织最终回答

这就是最小 Agent Loop。

2. 错误做法:把 Agent 写成一次模型请求

很多早期实现会这样写:

async function askAgent(question: string) {
  const prompt = `
你是一个智能助手,请完成用户任务。

用户问题:${question}
`;

  return llm.chat(prompt);
}

这段代码的问题是:它没有工具执行,也没有状态回写。

模型只能生成一段回答。
如果答案涉及订单、库存、权限、合同、退款金额,它很可能是在“猜”。

另一种常见错误,是只执行一次工具,然后直接把工具 JSON 返回给用户:

async function askOrderOnce(orderId: string) {
  const result = await getOrderStatus({ orderId });
  return JSON.stringify(result);
}

这看起来接了工具,但还不是 Agent Loop。

因为它缺了最后一步:让模型基于工具结果组织面向用户的回答。
也缺了更重要的一点:当模型还需要第二个工具时,系统没有继续循环的能力。

3. 正确做法:把模型输出设计成两种类型

在普通聊天里,模型输出通常就是一段文本。

但在 Agent 里,模型输出至少有两种可能:

type ModelOutput =
  | {
      type: "tool_call";
      toolName: string;
      args: Record<string, unknown>;
    }
  | {
      type: "final_answer";
      content: string;
    };

第一种是工具调用:

{
  "type": "tool_call",
  "toolName": "getOrderStatus",
  "args": {
    "orderId": "A1001"
  }
}

意思是:现在信息不够,需要程序帮我调用工具。

第二种是最终回答:

{
  "type": "final_answer",
  "content": "订单 A1001 当前运输中,预计 2026-05-03 送达。"
}

意思是:我已经可以回答用户了。

Agent Loop 的核心判断就一句话:

如果是 tool_call,就执行工具,然后继续。
如果是 final_answer,就返回答案,然后结束。

复杂 Agent 会加入规划、记忆、多工具、多轮任务,但地基仍然是这个判断。

4. 先用 mockModel 练骨架,别一上来接真实 API

很多人一上来就接真实模型,结果调试时一团乱:

  • 是 Prompt 不清楚?
  • 是模型没有按格式返回?
  • 是工具调度器有 bug?
  • 是参数校验拦错了?
  • 是 API 调用失败?
  • 是网络或环境变量问题?

更稳的做法是:先用 mockModel

就像前端开发时,后端接口没好,你会先 mock 数据,把页面状态和交互跑通。

Agent 也一样。

type Message =
  | {
      role: "user";
      content: string;
    }
  | {
      role: "tool";
      toolName: string;
      content: unknown;
    };

function mockModel(messages: Message[]): ModelOutput {
  const lastMessage = messages[messages.length - 1];

  if (lastMessage.role === "user") {
    if (lastMessage.content.includes("A1001")) {
      return {
        type: "tool_call",
        toolName: "getOrderStatus",
        args: { orderId: "A1001" }
      };
    }

    return {
      type: "final_answer",
      content: "我暂时只能处理订单 A1001 的查询。"
    };
  }

  if (lastMessage.role === "tool") {
    const result = lastMessage.content as {
      status?: string;
      eta?: string;
    };

    return {
      type: "final_answer",
      content: `订单当前状态为 ${result.status},预计 ${result.eta} 送达。`
    };
  }

  return {
    type: "final_answer",
    content: "无法处理当前请求。"
  };
}

它不聪明,但它稳定。

稳定的好处是,你可以先验证 Agent Loop 的工程结构:

  • messages 是否维护正确。
  • 工具调用是否被执行。
  • 工具结果是否写回上下文。
  • 最终回答是否能结束循环。
  • 错误路径是否会返回。

等骨架跑稳了,再接真实模型,问题会清楚很多。

5. messages 是 Agent 的短期记忆

Agent Loop 里最重要的数据结构之一是 messages

它记录了整个运行过程:

const messages: Message[] = [
  {
    role: "user",
    content: "帮我查订单 A1001"
  },
  {
    role: "tool",
    toolName: "getOrderStatus",
    content: {
      status: "shipping",
      eta: "2026-05-03"
    }
  }
];

为什么工具结果要写回 messages

因为模型下一轮需要看到它。

如果工具执行完了,但你没有把结果放回上下文,模型就像刚查完资料却失忆:

工具查到了,但模型不知道查到了什么。

这类 bug 在 Agent 开发里非常常见。

记住一个规则:

工具执行不是终点,工具结果必须回到上下文。

6. 一个最小 Agent Loop 长这样

先准备一个工具和调度器:

type ToolResult =
  | {
      ok: true;
      toolName: string;
      data: unknown;
    }
  | {
      ok: false;
      toolName?: string;
      error: string;
    };

async function getOrderStatus(args: Record<string, unknown>) {
  if (args.orderId !== "A1001") {
    return { status: "not_found" };
  }

  return {
    status: "shipping",
    eta: "2026-05-03"
  };
}

async function runTool(output: Extract<ModelOutput, { type: "tool_call" }>) {
  if (output.toolName !== "getOrderStatus") {
    return {
      ok: false,
      toolName: output.toolName,
      error: `未知工具:${output.toolName}`
    } satisfies ToolResult;
  }

  const data = await getOrderStatus(output.args);

  return {
    ok: true,
    toolName: output.toolName,
    data
  } satisfies ToolResult;
}

然后写 Agent Loop:

type AgentRunResult =
  | {
      ok: true;
      answer: string;
      steps: number;
      messages: Message[];
    }
  | {
      ok: false;
      error: string;
      steps: number;
      messages: Message[];
    };

async function runAgentLoop(
  userInput: string,
  maxSteps = 3
): Promise<AgentRunResult> {
  const messages: Message[] = [
    {
      role: "user",
      content: userInput
    }
  ];

  for (let step = 1; step <= maxSteps; step++) {
    const output = mockModel(messages);

    if (output.type === "final_answer") {
      return {
        ok: true,
        answer: output.content,
        steps: step,
        messages
      };
    }

    if (output.type === "tool_call") {
      const toolResult = await runTool(output);

      messages.push({
        role: "tool",
        toolName: output.toolName,
        content: toolResult.ok ? toolResult.data : toolResult
      });

      continue;
    }

    return {
      ok: false,
      error: "模型输出了未知类型",
      steps: step,
      messages
    };
  }

  return {
    ok: false,
    error: "超过最大步骤数,Agent 已停止",
    steps: maxSteps,
    messages
  };
}

这段代码已经包含 Agent 的核心骨架:

  • 有用户输入。
  • 有模型决策。
  • 有工具执行。
  • 有上下文更新。
  • 有最终回答。
  • 有最大步数。
  • 有异常退出。

复杂 Agent 往后扩展,基本都是在这个骨架上继续加能力。

7. maxSteps 是 Agent 的安全绳

为什么一定要有 maxSteps

因为模型可能会反复调用工具。

比如一个异常模型每次都返回:

{
  "type": "tool_call",
  "toolName": "searchPolicy",
  "args": {
    "keyword": "报销"
  }
}

如果你没有最大步数,Agent 就会一直查,一直查,一直查。

这不是智能,这是迷路。

maxSteps 就像安全绳:

最多跑 3 步,跑不完就停下来,把错误返回。

玩具 demo 假设模型总是乖。

工程系统默认任何环节都可能出错。

8. 前端页面怎么展示 Agent Loop?

如果你只展示最终回答,排查 Agent 问题会很痛苦。

建议至少在开发环境或内部后台展示步骤 trace。

type AgentTraceStep = {
  step: number;
  modelOutput: ModelOutput;
  toolResult?: ToolResult;
};

Agent Loop 运行时记录 trace:

async function runAgentLoopWithTrace(userInput: string, maxSteps = 3) {
  const messages: Message[] = [{ role: "user", content: userInput }];
  const trace: AgentTraceStep[] = [];

  for (let step = 1; step <= maxSteps; step++) {
    const output = mockModel(messages);
    const traceStep: AgentTraceStep = {
      step,
      modelOutput: output
    };

    if (output.type === "final_answer") {
      trace.push(traceStep);
      return {
        ok: true,
        answer: output.content,
        messages,
        trace
      };
    }

    const toolResult = await runTool(output);
    traceStep.toolResult = toolResult;
    trace.push(traceStep);

    messages.push({
      role: "tool",
      toolName: output.toolName,
      content: toolResult.ok ? toolResult.data : toolResult
    });
  }

  return {
    ok: false,
    error: "超过最大步骤数,Agent 已停止",
    messages,
    trace
  };
}

前端可以把 trace 做成折叠面板:

function AgentTracePanel({ trace }: { trace: AgentTraceStep[] }) {
  return (
    <details>
      <summary>Agent 执行过程</summary>
      {trace.map((item) => (
        <section key={item.step}>
          <h4>Step {item.step}</h4>
          <pre>{JSON.stringify(item.modelOutput, null, 2)}</pre>
          {item.toolResult && (
            <pre>{JSON.stringify(item.toolResult, null, 2)}</pre>
          )}
        </section>
      ))}
    </details>
  );
}

这不是炫技,而是为了让 Agent 出问题时能定位:

  • 模型哪一步选错了工具?
  • 参数是哪一步变错的?
  • 工具有没有执行成功?
  • 工具结果有没有回到上下文?
  • 为什么没有生成最终回答?

9. Agent Loop 不是无限自治,而是受控协作

很多人喜欢把 Agent 描述成“自主智能体”。

这个词没问题,但容易让初学者误会:好像越自主越高级。

工程里不是这样。

真正可靠的 Agent,不是无限自治,而是受控协作。

它应该有:

  • 明确的工具范围。
  • 明确的参数 Schema。
  • 明确的最大步数。
  • 明确的错误处理。
  • 明确的风险拦截。
  • 明确的日志和 traceId。

模型可以决定下一步,但只能在程序允许的范围内决定。

对前端开发者来说,可以这样类比:

用户输入 = 用户事件
messages = 状态容器
modelOutput = 状态决策
tool_call = effect 描述
runTool = effect 执行器
final_answer = 渲染结果
maxSteps = 防止无限循环的保护

Agent 不是凭空多出来的外星架构。

它只是把语言模型放进了你熟悉的状态与副作用系统里。

10. 生产环境避坑指南

1. 不要让 Agent 无限循环

必须设置 maxSteps
超过步数就停止,并返回结构化错误。

2. 不要把 tool error 当正常工具结果

工具失败时,要让模型知道这是失败,而不是把错误文本当成正常数据继续编。

建议在 tool message 里保留 okerrorTypemessage 等字段。

3. 高风险工具不要自动执行

取消订单、发送邮件、修改权限、扣款、删除数据,都不应该在 Agent Loop 中无确认执行。

这类工具应该返回 confirmation_required,交给用户或业务规则确认。

4. 每一步都要记录 traceId

一次 Agent 请求可能包含多次模型调用和多次工具调用。

没有统一 traceId,排查时很难把它们串起来。

5. 步数越多,不代表能力越强

步数越多,成本、延迟和错误概率都会上升。

大多数业务型 Agent,先把 2 到 4 步跑稳定,比追求长链路自主规划更重要。

11. 常见误区

误区 1:一次大模型调用就是 Agent

不是。一次模型调用只是聊天或生成。Agent 至少要有决策、工具、状态和循环。

误区 2:工具执行完就结束

不一定。工具结果通常还要回到模型,让模型生成用户能读懂的最终回答。

误区 3:Agent 应该尽可能多跑几步

不是。步数越多,成本、延迟和错误概率都会上升。够用就好。

误区 4:先接真实模型才能学 Agent

不需要。先用 mock 模型跑通控制流,反而更适合学习和调试。

12. 给前端开发者的落地清单

如果你要在团队里实现一个最小 Agent,可以从这份清单开始:

  1. 明确 Agent 支持哪些任务。
  2. 定义可用工具和工具 Schema。
  3. 实现工具调度器。
  4. 设计模型输出类型:tool_callfinal_answer
  5. 维护 messages
  6. 工具结果写回上下文。
  7. 设置 maxSteps
  8. 记录每一步 trace。
  9. 处理未知工具、参数错误和工具失败。
  10. 高风险工具必须走确认。

这份清单能帮你避免把 Agent 做成一个“看起来会自己想办法,实际上出了事没人知道在哪里”的黑盒。

结语

Agent 不是一次调用。

Agent 是一轮受控循环。

模型负责判断下一步,工具负责获取真实信息或执行动作,程序负责维护状态、控制边界、处理错误和决定什么时候停止。

对前端开发者来说,这件事并不陌生。你早就熟悉事件、状态、副作用和渲染。现在只是多了一个语言模型,帮助系统理解自然语言里的意图。

当你能写出一个最小 Agent Loop,能看懂每一步发生了什么,能让它在错误时停下来而不是乱跑,你就已经跨过了 Agent 工程最重要的一道门槛。

前端开发者做 Agent:Tool Calling 别只写函数名,用 Schema 少踩 5 个坑

作者:前端转 AI 深度实践者

【省流助手/核心观点】:Tool Calling 不是把几个函数名丢给模型就完事了。函数名只能告诉模型“有什么工具”,但不能说明“什么时候用、怎么传参、哪些字段必填、风险有多高、能不能自动执行”。真正可维护的 Agent 工程,需要给每个工具写一份 Schema:描述用途、约束参数、标记风险、控制确认流程。对前端开发者来说,这就像给 API 写 TypeScript 类型、接口文档、表单校验和权限边界。


第 23 篇我们讲了 Agent 的第一块积木:Tool Calling。

简单说,就是让模型不再硬编答案,而是学会提出工具调用意图。

用户问:

帮我查一下订单 A1001 到哪了。

模型不应该直接编:

订单正在配送中。

而应该输出:

{
  "toolName": "getOrderStatus",
  "args": {
    "orderId": "A1001"
  }
}

然后程序真正去查订单系统。

这一步很关键,它把 AI 应用从“会聊天”推进到“能办事”。

但只做到这里还不够。

因为模型可能会:

  • 选错工具。
  • 少传参数。
  • 把数字传成字符串。
  • 把枚举值写错。
  • 调用根本不存在的工具。
  • 请求执行一个高风险操作。

所以这篇文章继续往下走一步:

Tool Calling 不能只靠函数名,还要给工具写 Schema。

这个 Schema,就是工具说明书。

1. 痛点:只注册函数,对程序够用,对模型不够用

先看一个很常见的工具注册表:

const tools = {
  getOrderStatus,
  calculateRefund,
  searchPolicy
};

这对程序来说确实能跑。

程序看到 getOrderStatus,就能找到对应函数执行。

但对模型来说,它只看到几个名字。
这些名字没有完整说明:

  • getOrderStatus 是查物流,还是查订单详情?
  • calculateRefund 是预估退款,还是直接发起退款?
  • searchPolicy 是查公司制度,还是查售后政策?
  • 每个工具需要哪些参数?
  • 参数类型是什么?
  • 哪些参数允许哪些枚举值?
  • 哪些工具只是查询,哪些工具会改变系统状态?

这就像你给新同事一个接口列表:

/order
/refund
/policy

然后让他自己猜每个接口怎么用。

他可能猜对,也很容易猜错。

Agent 系统一样。工具名不是工具契约,工具名只是入口。

2. 错误做法:模型输出什么,程序就执行什么

很多人第一次写 Tool Calling,会不自觉地相信模型输出:

async function unsafeRunTool(toolCall: {
  toolName: string;
  args: Record<string, unknown>;
}) {
  const tool = tools[toolCall.toolName as keyof typeof tools];
  return tool(toolCall.args);
}

这段代码最大的问题是:没有任何边界。

模型可能传错类型:

{
  "toolName": "getOrderStatus",
  "args": {
    "orderId": 1001
  }
}

但你的工具需要的是字符串:

async function getOrderStatus(args: { orderId: string }) {
  // ...
}

模型也可能少传参数:

{
  "toolName": "calculateRefund",
  "args": {
    "reason": "damaged"
  }
}

但退款计算至少需要 orderId

它还可能传错枚举值:

{
  "toolName": "calculateRefund",
  "args": {
    "orderId": "A1001",
    "reason": "随便退一下"
  }
}

如果你直接执行,错误会在很深的业务代码里爆炸。更糟的是,高风险工具可能被误触发。

所以你需要工具 Schema。

3. 正确做法:Schema = 接口文档 + 类型定义 + 安全边界

一个更可靠的工具定义,应该同时包含 handler 和 schema。

type ToolRisk = "low" | "medium" | "high";

type ToolSchema = {
  name: string;
  description: string;
  risk: ToolRisk;
  requiresConfirmation: boolean;
  parameters: {
    type: "object";
    required: string[];
    properties: Record<
      string,
      {
        type: "string" | "number" | "boolean";
        description: string;
        enum?: string[];
        pattern?: string;
      }
    >;
  };
};

type ToolDefinition = {
  schema: ToolSchema;
  handler: (args: Record<string, unknown>) => Promise<unknown>;
};

比如 getOrderStatus 可以这样写:

const getOrderStatusTool: ToolDefinition = {
  schema: {
    name: "getOrderStatus",
    description:
      "查询订单物流状态。适用于用户询问订单是否发货、是否签收、预计何时送达。不用于退款、取消订单或修改地址。",
    risk: "low",
    requiresConfirmation: false,
    parameters: {
      type: "object",
      required: ["orderId"],
      properties: {
        orderId: {
          type: "string",
          description: "订单编号,例如 A1001",
          pattern: "^A\d{4}$"
        }
      }
    }
  },
  handler: async (args) => {
    const orderId = args.orderId;
    if (typeof orderId !== "string") {
      throw new Error("orderId 必须是字符串");
    }

    return {
      orderId,
      status: "shipping",
      eta: "2026-04-28"
    };
  }
};

这份 Schema 至少告诉系统 5 件事:

  1. 工具是做什么的。
  2. 什么时候该用这个工具。
  3. 参数有哪些,哪些必填。
  4. 参数类型和格式是什么。
  5. 工具有没有风险,能不能直接执行。

如果你是前端开发者,可以把它理解成:

Tool Schema = API 文档 + TypeScript 类型 + 表单校验规则 + 权限提示

没有 Schema 的 Tool Calling,就像没有类型定义的接口联调。能跑,但迟早会在边界条件上摔跤。

4. 好的 description,会显著减少选错工具

很多人写工具描述,会写得很短:

查询订单。

这当然比没有好,但还不够。

更好的描述要包含适用场景和不适用场景:

查询订单物流状态。适用于用户询问订单是否发货、是否签收、预计何时送达。不用于退款、取消订单或修改地址。

这个描述告诉模型:

  • 用户问物流,选它。
  • 用户问退款,不要选它。
  • 用户问取消订单,也不要选它。

工具描述不是写给人看的注释而已。

它会影响模型的选择质量。你可以把工具描述理解成一种挂在工具上的 Prompt。

5. 参数校验:模型输出的 args 本质上是不可信输入

args 是模型生成的,所以它应该被当成用户输入处理。

一个最小参数校验器可以这样写:

function validateArgs(schema: ToolSchema, args: Record<string, unknown>) {
  const errors: string[] = [];
  const { required, properties } = schema.parameters;

  for (const key of required) {
    if (!(key in args)) {
      errors.push(`缺少必填参数:${key}`);
    }
  }

  for (const [key, rules] of Object.entries(properties)) {
    const value = args[key];
    if (value === undefined) continue;

    if (rules.type === "string" && typeof value !== "string") {
      errors.push(`参数 ${key} 应该是 string`);
      continue;
    }

    if (rules.type === "number" && typeof value !== "number") {
      errors.push(`参数 ${key} 应该是 number`);
      continue;
    }

    if (rules.type === "boolean" && typeof value !== "boolean") {
      errors.push(`参数 ${key} 应该是 boolean`);
      continue;
    }

    if (rules.enum && !rules.enum.includes(String(value))) {
      errors.push(`参数 ${key} 必须是 ${rules.enum.join(", ")} 之一`);
    }

    if (
      rules.pattern &&
      typeof value === "string" &&
      !new RegExp(rules.pattern).test(value)
    ) {
      errors.push(`参数 ${key} 格式不合法`);
    }
  }

  return errors;
}

这段代码看起来不酷,但它是 Agent 稳定性的地基。

AI 工程里很多真正有价值的代码,都不是“让模型更聪明”,而是让系统在模型不稳定时也不会乱跑。

6. 调度器要像网关,而不是传声筒

没有 Schema 校验时,调度器像一个传声筒:

模型说调用什么,我就调用什么。

更好的调度器应该像网关:

模型提出调用意图
-> 检查工具是否存在
-> 检查参数是否合法
-> 检查风险等级
-> 检查是否需要确认
-> 决定是否执行

一个升级版 runTool 可以这样写:

type ToolCall = {
  toolName: string;
  args: Record<string, unknown>;
};

type ToolRunResult =
  | {
      ok: true;
      toolName: string;
      data: unknown;
    }
  | {
      ok: false;
      toolName?: string;
      errorType:
        | "unknown_tool"
        | "invalid_arguments"
        | "confirmation_required"
        | "tool_error";
      message: string;
      errors?: string[];
    };

const toolRegistry: Record<string, ToolDefinition> = {
  getOrderStatus: getOrderStatusTool
};

async function runTool(toolCall: ToolCall): Promise<ToolRunResult> {
  const tool = toolRegistry[toolCall.toolName];

  if (!tool) {
    return {
      ok: false,
      toolName: toolCall.toolName,
      errorType: "unknown_tool",
      message: `未知工具:${toolCall.toolName}`
    };
  }

  const errors = validateArgs(tool.schema, toolCall.args ?? {});
  if (errors.length > 0) {
    return {
      ok: false,
      toolName: tool.schema.name,
      errorType: "invalid_arguments",
      message: "工具参数不合法",
      errors
    };
  }

  if (tool.schema.requiresConfirmation) {
    return {
      ok: false,
      toolName: tool.schema.name,
      errorType: "confirmation_required",
      message: "该工具属于高风险操作,需要用户确认后才能执行"
    };
  }

  try {
    const data = await tool.handler(toolCall.args);
    return {
      ok: true,
      toolName: tool.schema.name,
      data
    };
  } catch (error) {
    return {
      ok: false,
      toolName: tool.schema.name,
      errorType: "tool_error",
      message: error instanceof Error ? error.message : "工具执行失败"
    };
  }
}

这段逻辑让系统多了几道闸门。

这些闸门不是为了为难模型,而是为了保护用户、业务和团队。

7. 查询类工具和写入类工具必须分开看

Tool Calling 里最需要警惕的是副作用。

查询类工具通常风险较低:

  • 查订单状态。
  • 查制度文档。
  • 查天气。
  • 查库存。

它们只是读取信息,不改变系统状态。

写入类工具就不一样了:

  • 取消订单。
  • 发起退款。
  • 修改地址。
  • 发送邮件。
  • 删除数据。
  • 修改用户权限。

这些操作会改变真实业务状态。

如果模型误触发,后果可能很麻烦。

所以你应该给工具标记风险:

const cancelOrderTool: ToolDefinition = {
  schema: {
    name: "cancelOrder",
    description:
      "取消指定订单。仅当用户明确要求取消订单时使用。不用于查询订单状态或咨询退款政策。",
    risk: "high",
    requiresConfirmation: true,
    parameters: {
      type: "object",
      required: ["orderId", "reason"],
      properties: {
        orderId: {
          type: "string",
          description: "订单编号,例如 A1001",
          pattern: "^A\d{4}$"
        },
        reason: {
          type: "string",
          description: "取消原因",
          enum: ["user_request", "wrong_address", "duplicate_order"]
        }
      }
    }
  },
  handler: async (args) => {
    return {
      cancelled: true,
      orderId: args.orderId
    };
  }
};

当模型试图调用高风险工具时,系统不应该立刻执行,而应该返回确认态。

这不是“不智能”,这是负责任。

AI 工程里有一个很实用的原则:

能查的,可以宽一点;能改的,必须严一点。

8. 生产环境避坑指南

1. Schema 要和真实 handler 同步维护

最危险的情况是:Schema 说需要 orderId,handler 实际读的是 id

这类错不会总是立刻暴露,但会让模型、前端和后端一起困惑。

建议把 Schema 和 handler 放在同一个文件或同一个模块里维护,不要散落在不同仓库里。

2. 不要用模糊工具名

坏名字:

doUserThing
handleOrder
processData

好名字:

getOrderStatus
calculateRefundEstimate
searchPolicyDocs
createSupportTicketDraft

名字越清楚,模型越容易选对,团队也越容易维护。

3. description 要写“不适用场景”

很多工具选错,不是因为模型不知道它能做什么,而是不知道它不能做什么。

描述里最好写清楚:

  • 适用于什么问题。
  • 不适用于什么问题。
  • 和相似工具的区别是什么。

4. 高风险工具必须程序层拦截

不要只在 Prompt 里写“谨慎使用”。

Prompt 是软约束。权限校验、二次确认、审计日志才是硬约束。

涉及删除、支付、发送、修改权限、批量操作的工具,都应该默认需要确认。

5. 结构化错误要返回给模型和日志

不要只返回:

调用失败。

更好的错误是:

{
  "ok": false,
  "errorType": "invalid_arguments",
  "message": "工具参数不合法",
  "errors": ["缺少必填参数:orderId"]
}

这样模型可以尝试修正参数,开发者也能快速定位问题。

9. 常见误区

误区 1:函数名写清楚就够了

不够。函数名只能表达一小部分语义,不能替代参数规则、风险等级和适用边界。

误区 2:模型很聪明,参数错了它会自己修

有时会,但不能依赖。工程系统要把错误显式返回,而不是期待模型每次都猜对。

误区 3:高风险工具只要 Prompt 写“谨慎使用”就行

不行。Prompt 是软约束,权限和确认是硬约束。涉及副作用的工具必须在程序层把关。

误区 4:Schema 越复杂越好

也不是。初期 Schema 要清晰、够用、容易维护。复杂度应该来自真实问题,而不是一开始就堆满规则。

10. 给前端开发者的落地清单

如果你正在设计 Agent 工具,可以从这份清单开始:

  1. 每个工具必须有清晰名字。
  2. 每个工具必须有 description,包含适用和不适用场景。
  3. 每个工具必须声明必填参数。
  4. 每个参数必须有类型、描述和必要的格式约束。
  5. 枚举参数必须列出允许值。
  6. 工具必须标记风险等级。
  7. 高风险工具默认需要确认。
  8. 调度器必须返回结构化错误。
  9. 工具调用必须记录 traceId。
  10. 工具 Schema 要和代码一起维护。

这份清单不花哨,但很实用。它会让你的 Agent 不只是演示时聪明,而是在真实用户、真实业务、真实错误里也能站得住。

结语

Tool Calling 的第一步,是让模型知道有哪些工具。

Tool Calling 的第二步,是让系统知道这些工具该怎么被安全、稳定、可维护地使用。

这就是 Schema 的价值。

它像一份工具说明书,也像一份协作契约:告诉模型什么时候该用,告诉程序怎么校验,告诉团队风险在哪里。

如果说第 23 篇让我们把 Agent 的“手”接上去,那么第 24 篇就是给这只手标上边界、权限和刹车。

真正可靠的 Agent,不是模型想做什么就做什么。

真正可靠的 Agent,是模型提出建议,程序负责把关,工具在清晰边界内执行。

RAG 落地 3 个月,我才发现排序(Rerank)比检索更重要

作者:前端转 AI 深度实践者

【省流助手/核心观点】:RAG 系统的精度瓶颈往往不在 Embedding 检索,而在排序。语义检索(一阶段)只能保证“相关”,但不能保证“最优”。引入 Rerank(二阶段重排序),将最精准的资料排在最前面,能显著提升模型回复的贴题度,解决 AI “答非所问”的顽疾。


1. 痛点:为什么你的 AI 总是“差一点”?

作为前端开发者,我们习惯了 Array.sort()。但在 AI 知识库场景中,排序的失效会导致灾难。

你是否遇到过这种情况:

  • AI 回答没报错,但重点全偏了
  • 引用了资料,但引用的是过时的、次要的段落。
  • 感觉模型“理解力不行”,其实是它看到的上下文(Context)不对

真相是:模型也有“注意力局限”。 如果你把最重要的答案排在 Top 5 的最后一名,受限于上下文窗口和位置偏差(Lost in the Middle),模型极大概率会忽略它。


2. 代码实战:一阶段检索 vs 二阶段检索(Rerank)

我们可以把 Rerank 类比为前端面试的“初筛”与“技术终面”。

❌ 错误做法:直接拿 Embedding 结果喂给模型

只靠向量相似度,容易被“关键词重合”但业务无关的片段干扰。

# 伪代码:一阶段检索直接收工
raw_results = vector_db.search(query_vector, limit=5)
# 风险:Top 1 可能是个无关的 FAQ,真正的 API 文档排在 Top 5,模型漏看了

✅ 正确做法:检索(Top 20) + Rerank(Top 5)

先“广撒网”,再用专业的重排序模型进行“精挑选”。

# 1. 第一阶段:快速召回(向量检索)
initial_results = vector_db.search(query_vector, limit=20)

# 2. 第二阶段:Rerank 精排
# 使用类似 BGE-Reranker 的模型对 query 和 doc 进行交叉评分
reranked_results = reranker_model.predict(
    query=user_query,
    documents=[res.text for res in initial_results]
)

# 3. 截取最高分的 Top 5 喂给大模型
final_context = reranked_results[:5]
# 收益:最核心、最贴题的资料现在稳稳地坐在 Top 1 的位置

3. 生产环境避坑指南

在真实业务中落地 Rerank,请务必关注这 3 点:

  1. 延迟与精度的权衡:Rerank 是交叉编码器(Cross-Encoder),计算量比向量检索大得多。建议:召回阶段取 20-30 个片段即可,不要全量 Rerank,否则接口响应会从 200ms 飙升到 2s。
  2. 模型选型建议:不要自己训练,优先使用开源方案。国内推荐 BGE-Reranker,海外推荐 Cohere Rerank。对于中文业务,BGE 的表现非常惊艳。
  3. 注意上下文“噪音”:Rerank 的分值通常是 0-1 的概率值。如果最高分也低于 0.3,说明知识库里可能真的没有答案,此时应直接触发“不知道”逻辑,而不是强行让 AI 瞎猜。

4. 逻辑校正:排序不是装饰,是决策依据

很多团队容易陷入“改 Prompt”的死循环。

对读者的建议:当你觉得 AI 回答不准时,第一步不是改 Prompt,而是打印出检索回来的 Top 3 资料

  • 如果前三名里没有正确答案 -> 去优化 Embedding切块逻辑
  • 如果正确答案在第四、五名 -> 赶紧加上 Rerank

排序的本质是减少模型面对的熵(混乱度)。 给模型看最干净、最直接的证据,它才能给出最专业的回答。


结语

RAG 不只是找资料,还要把最关键的资料“递”到模型嘴边。

从“有没有”走向“准不准”,是每一个 AI 工程化团队的必经之路。 如果你的系统还在“差一点”的泥潭里挣扎,不妨试试 Rerank,这可能是你性价比最高的一次优化。


点赞 + 收藏不迷路,带你持续解锁前端转型 AI 的工程干货!

❌