前端工程师学 Agent(一):别从 Prompt 开始

大模型最初进入前端开发时,更像一个随时在线的问答助手:解释报错、补全函数、生成组件。

现在,它开始读取仓库、调用工具、运行测试,甚至操作浏览器。能力从“说”走向“做”以后,一个错误判断不再只产生一段错误文字,还可能变成真实动作。

这也是前端工程师需要理解 Agent 的原因。Prompt 仍然重要,但它只解决模型“怎么回答”。要让模型参与真实研发,还要回答另一组问题:下一步由谁选择,工具由谁执行,结果如何进入下一轮,任务又在什么条件下停止。

“前端工程师学 Agent”系列会沿一套前端研发 Agent 逐步推进:先完成只读调查,再进入代码审查,最后才在隔离环境中生成候选修复。系列使用 TypeScript 表达契约与控制流;案例和运行记录属于合成教学材料,不代表仓库中存在对应的可运行产品。

第一篇先把最小闭环搭起来:一次模型调用、两个只读工具,以及一个会根据新材料选择下一步的 Agent Loop。

从一句白屏反馈开始

先把下面这句话交给模型:

页面打开后白屏,只在部分用户身上出现。

模型很容易列出一串可能性:接口数据为空、浏览器兼容性、懒加载失败、路由异常。

这些解释都合理,却没有一句来自当前项目。

如果再补一条控制台报错,情况会发生变化:

Cannot read properties of undefined (reading 'name')
at UserCard (src/components/UserCard.tsx:42:31)

现在可以先查 UserCard.tsx。打开 42 行后,又看到:

return<span>{response.user.name}</span>;

这时仍然不能宣布根因。response.user 为什么缺失,还要继续检查请求结果或类型来源。

每多一份材料,下一步都可能改变。Agent 最小的轮廓就在这里:它不急着一次写完答案,而是根据当前材料选择下一个动作。

Agent 和普通模型调用差在哪里

普通模型调用更像一个不稳定的函数:传入报错,得到一份初步分析。

“不稳定”包含两层意思。

第一层是内容不稳定。模型可能提到一个看起来很合理的 src/hooks/useUser.ts,但输入里没有这个文件,系统也没有搜索过仓库。

第二层是结构不稳定。前端期待 nextChecks,模型这次却没有返回;页面如果直接相信结果,就可能在展示 AI 分析时再次报错。

因此,模型返回值应该从 unknown 开始,在运行时经过 Schema 校验:

import { z } from"zod";

const errorAnalysisSchema = z.object({
summary: z.string().min(1),
suspectedFiles: z.array(z.string()).max(10),
possibleCauses: z.array(z.string()).min(1).max(5),
nextChecks: z.array(z.string()).min(1).max(5),
confidence: z.enum(["low""medium""high"]),
});

typeErrorAnalysis = z.infer<typeof errorAnalysisSchema>;

这里有一个容易混淆的边界:Schema 只能证明数据形状正确,不能证明内容真实。

suspectedFiles 是字符串数组,不代表数组里的文件真的存在;confidence: "high" 也不代表项目根因已经确认。没有读取代码时,界面应该明确显示“初步分析”,而不是“调查完成”。

普通模型调用停在这里。它可以生成结构化结果,却不会因为新材料自动继续调查。

给模型工具,不等于给它终端

要让调查继续,第一版只需要两个很窄的动作:

  • • 在允许的源码目录中搜索文本;
  • • 读取某个命中文件的指定行。

模型先提出工具请求:

{
"name":"search_code",
"arguments":{
"query":"UserCard"
}
}

这段 JSON 不是终端命令。真实链路更接近:

模型提出工具请求
→ 应用校验工具名和参数
→ 执行器检查目录与权限
→ 工具执行搜索
→ 应用把结果交回模型

常说的“模型调用工具”,其实省略了中间最重要的一层。模型只负责生成请求,真正打开文件、运行命令或访问网络的,始终是受控的应用程序。

工具参数也要尽量窄:

const toolRequestSchema = z.discriminatedUnion("name", [
  z.object({
name: z.literal("search_code"),
arguments: z.object({
query: z.string().min(1).max(100),
    }),
  }),
  z.object({
name: z.literal("read_file"),
arguments: z.object({
path: z.string().min(1),
startLine: z.number().int().positive(),
endLine: z.number().int().positive(),
    }),
  }),
]);

search_code 不接收整条 rg 命令,read_file 也不接收任意 Shell。模型可以决定搜什么、读哪一段,程序则固定工作目录、超时和输出上限。

参数 Schema 仍不是全部安全边界。path 即使是合法字符串,也可能包含 ../../,或通过软链接指向仓库外。执行器还要解析真实路径,再确认目标仍位于本次任务允许的根目录中。

换句话说,Prompt 可以提醒模型不要越界,执行层才负责让越界动作真的无法发生。

两个工具还不是 Agent

有了 search_code 和 read_file,系统已经不用猜文件。但每次工具执行后,如果仍要由人决定下一步,它还只是一个带 AI 能力的工具箱。

Agent Loop 做的事情,是把每次工具结果放回任务状态,再请模型选择下一步。

先把动作限制为三种:

typeAction =
  | { type"search_code"querystring }
  | { type"read_file"pathstringstartLinenumberendLinenumber }
  | {
type"finish";
conclusionstring;
evidenceRefsstring[];
unknownsstring[];
    };

finish 不能只带一句结论。它还要说明结论引用了哪些工具结果,以及还有什么没有确认。否则模型随时可以跳过调查,把最初的猜测写成完成结果。

最小循环并不长:

typeStepRecord = {
actionExclude<Action, { type"finish" }>;
resultToolResult;
};

asyncfunctioninvestigate(goalstringsignalAbortSignal) {
consthistoryStepRecord[] = [];
const seen = newSet<string>();

for (let step = 0; step < 8; step += 1) {
    signal.throwIfAborted();

const action = await model.chooseNextAction({ goal, history });

if (action.type === "finish") {
if (action.evidenceRefs.length === 0) {
thrownewError("结论没有引用调查材料");
      }
return action;
    }

const key = JSON.stringify(action);
if (seen.has(key)) {
thrownewError("重复执行同一动作,调查已停止");
    }

    seen.add(key);
const result = awaitexecuteAllowedTool(action, { signal });
    history.push({ action, result });
  }

thrownewError("调查超过步数上限");
}

这是教学示意,不是可以直接上线的完整实现。模型调用与工具还需要各自的超时,步骤记录也需要持久化。但它已经暴露了 Agent 最重要的控制点:

  • • 模型每轮只选择一个外部动作;
  • • 工具始终由可信执行器调用;
  • • 新材料进入下一轮上下文;
  • • 重复、取消和预算耗尽都会停止循环;
  • • 完成结论必须带证据和未知项。

一次调查怎样改变方向

用一份简化步骤记录检查这个循环:

1 search_code("UserCard")
  → 找到 src/components/UserCard.tsx

2 read_file("src/components/UserCard.tsx", 35, 48)
  → 发现直接读取 response.user.name

3 search_code("response.user")
  → 找到 ProfileResponse 类型和请求适配层

4 read_file("src/api/profile.ts", 1, 70)
  → 访客响应允许 user 缺失

5 finish
  → 结论引用第 2、4 步;尚未运行浏览器复现

第 2 步之后,模型没有继续围着 UserCard 搜索,而是把问题转向数据契约。路线因为新材料发生了变化,这一点比“循环执行了五步”更重要。

一次调查也不只有“成功”这一种停止状态。搜索范围不包含所需仓库、模型反复执行同一动作、预算耗尽、路径越界或者用户取消,都应该产生可区分的结果。

“材料不足”和“已确认根因”都可以结束任务,却不能共用一个绿色完成标记。

什么时候根本不需要 Agent

如果任务只是把报错整理成结构化摘要,一次模型调用就够了。

如果执行路线已经确定,例如固定执行“类型检查 → Lint → 单测”,普通 Workflow 更容易测试,也更容易估算成本。

只有当下一步必须根据刚得到的材料重新选择时,Agent Loop 才真正有价值。

判断一个功能是否需要 Agent,可以先问三个问题:

  1. 1. 一次生成能否完成?
  2. 2. 多步执行的顺序能否提前写好?
  3. 3. 新材料是否会改变下一步动作?

前两个问题回答“能”,通常不必增加 Agent。多步骤不是使用 Agent 的理由,路线必须在运行中变化才是。

本文总结

一个最小 Agent 不需要从复杂框架开始。先写清三件事就够了:

  • • 模型能够提出哪些动作;
  • • 应用允许执行哪些工具;
  • • 任务在什么条件下完成、失败或停止。

普通模型调用负责生成候选结果,Schema 保护数据形状;工具让模型取得项目材料,执行器守住权限;Agent Loop 则把新材料带回下一轮,让路线能够随现场变化。

模型负责选择,程序负责执行。这个边界一旦模糊,Agent 很快就会从“会调查”变成“会失控”。

 

分享到: 文章二维码
© 版权声明

暂无评论

您必须登录才能参与评论!
暂无评论...