跳转到正文

前端工程师的 Agent 开发路线

前端工程师进入 Agent 开发,最大的优势不是“会做聊天框”,而是已经熟悉状态、异步交互、错误恢复和用户反馈。真正需要补上的,是模型流、工具调用、安全边界和评测。

这条路线的目标不是一步做出全自动 Agent,而是先做出一个状态可解释、操作可中止、结果可验证的 Agent 产品。

先建立正确的心智模型

普通聊天应用通常只有“请求 → 文本回复”。Agent 多了一个循环:

text
用户输入

模型判断下一步
   ├── 直接回复 ────────────────┐
   └── 请求调用工具             │
            ↓                   │
       应用校验并执行工具        │
            ↓                   │
       把工具结果交还模型 ───────┘

        最终回复或继续循环

模型产生的工具调用只是一个提议。应用仍然负责参数校验、权限判断、执行、超时、重试、审计和用户确认。

Pi 的包应该怎么选

Pi 上游是一个 TypeScript monorepo。不同包服务于不同层,不要把它们全部装进 Vue 应用。

适合做什么前端项目中的建议
@earendil-works/pi-ai模型目录、供应商、消息、流式响应和工具 schema从这里开始;生产调用优先放在后端
@earendil-works/pi-agent-coreAgent 循环、状态、工具执行和事件需要自动工具循环时再加入
@earendil-works/pi-coding-agentPi CLI、终端会话、文件和 shell 工具不要直接打进浏览器
@earendil-works/pi-tui终端 UI 组件它不是 DOM/Vue 组件库
@earendil-works/pi-server实验性服务端包不要把生产架构建立在未稳定 API 上

一个好原则是:能用 pi-ai 解决,就先不要引入完整 Agent 循环;能用一个供应商,就不要导入所有供应商。

当前 API 使用 provider collection,而不是旧的全局 getModel() / stream() 风格:

ts
import { createModels } from "@earendil-works/pi-ai";
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";

const models = createModels();
models.setProvider(openaiProvider());

const model = models.getModel("openai", "<model-id>");
if (!model) {
  throw new Error("Model not found");
}

不要为了方便从 providers/all 导入全部供应商。它会扩大浏览器或服务端 bundle,也让可用能力边界更难审计。

如果你正在 Print、JSON、RPC 和 SDK 之间选择,先看自动化与程序集成;需要 Vue 状态骨架时再进入 SDK 与 Vue 最小集成

推荐的第一版架构

第一版不要让浏览器直接连接模型供应商:

text
Vue 3
├── 消息列表
├── 输入与停止按钮
├── 运行状态机
└── 流事件解析

        │  只调用你自己的同源 API

应用后端
├── 用户认证与限流
├── 模型、参数和工具白名单
├── pi-ai / pi-agent-core
├── 供应商密钥
└── 工具执行器


模型供应商与受控外部服务

为什么要有后端

  • VITE_* 环境变量会进入客户端 bundle,不能保存密钥。
  • 浏览器存储会被同源脚本读取,不能当作供应商密钥保险箱。
  • 工具执行需要服务端权限、超时、审计和资源限制。
  • 后端可以统一处理供应商切换、限流、成本和错误格式。

后端不一定复杂。第一版只需要一个经过认证的流式接口、一个模型白名单和零个工具。

用 Vue 设计 Agent 状态

不要只用 loading: boolean。先把整次运行生命周期每个工具调用拆开:

ts
type AgentRunState =
  | { status: "idle" }
  | { status: "connecting"; runId: string }
  | { status: "running"; runId: string }
  | { status: "done"; runId: string; reason: string }
  | { status: "aborted"; runId: string }
  | { status: "error"; runId: string; message: string };

interface ToolCallState {
  callId: string;
  name: string;
  status: "proposed" | "awaiting-approval" | "running" | "done" | "error";
  input?: unknown;
  output?: unknown;
  error?: string;
}

const toolCalls = new Map<string, ToolCallState>();

使用可辨识联合类型后,组件能穷尽处理每个状态,避免“按钮还在转,但请求早已失败”。工具用 callId 作为键,才能正确表达并行调用、独立确认和乱序完成;一个 toolName 字段无法承担这些语义。

Composable 还是 Pinia

建议按复杂度升级:

  1. 单页面、单会话:先用 useAgentRun() composable 和 ref
  2. 多组件共享同一会话:再引入 Pinia。
  3. 多会话、队列、跨路由恢复:把可序列化会话状态放进 Pinia。
  4. AbortController、网络连接和计时器留在 composable/service 中,不要持久化。

不要因为“技术栈要完整”就提前加入 Router、Pinia 和复杂状态框架。

正确处理流式事件

流式响应不只是文本增量。你还会遇到开始、思考内容、工具调用、完成、取消和错误事件。

实现时注意:

  • 按消息 ID 和内容块索引归并事件。
  • 不同内容块可能交错到达,不要假设所有 text_delta 连续。
  • 收到错误或取消后,保留已经到达的部分内容。
  • 区分 stoplengthtoolUseerroraborted
  • length 表示达到输出上限,不等于完整成功。
  • 工具参数的流式片段可能不是合法 JSON;等完整事件后再解析。

推荐把网络事件先转换成应用自己的稳定事件,再交给 Vue:

ts
type UiAgentEvent =
  | { type: "run-started"; runId: string }
  | { type: "text-appended"; messageId: string; text: string }
  | { type: "tool-proposed"; callId: string; name: string; input: unknown }
  | { type: "tool-finished"; callId: string; output: unknown }
  | { type: "run-finished"; reason: string }
  | { type: "run-failed"; message: string };

这样,Vue 组件不依赖供应商特有字段,替换模型或传输协议时不需要重写界面。

消息是会话树,不只是数组

界面可以按当前分支线性渲染,但数据模型应预留:

  • 稳定的 id,用于流事件归并、重试和回放。
  • parentId,用于分支、重新生成和方案对比。
  • 内容块数组,而不是把文本、思考和工具混成一个字符串。
  • 完成原因、模型与用量,便于解释截断和成本。
  • 压缩摘要的来源关系,避免静默丢失历史。

这样后续加入会话分支、DiffViewer、工具时间线或事件回放时,不必推翻最初的数据结构。

交互设计的最低标准

一个可用的 Agent 界面至少应做到:

  • 明确区分“正在连接、模型生成、等待确认、工具执行、完成、失败”。
  • 生成期间提供停止按钮,并用 AbortController 真正取消请求。
  • 停止后保留部分回复,允许用户继续追问。
  • 展示工具名称、关键参数、执行结果和耗时。
  • 并行工具分别展示状态,不用一个全局 spinner 掩盖全部进度。
  • 高风险工具执行前展示将要发生的具体变化。
  • 错误信息可操作,例如“认证失效,请重新登录”,而不是只显示“失败”。
  • 页面刷新后能恢复已确认保存的会话。
  • 键盘可操作,流式更新不会抢走焦点或让屏幕阅读器重复朗读整页。

WebSocket 并不天然比 SSE 或 Fetch Stream 更先进。第一版通常使用单向流就够了;只有确实需要双向实时控制时再增加 WebSocket。

工具调用是安全边界

模型输出不能直接变成函数调用,更不能直接拼成 shell 命令。

每个工具都应该有:

  1. 稳定且受控的工具名称。
  2. 明确的输入 schema。
  3. 服务端参数校验。
  4. 超时、取消和输出大小限制。
  5. 权限与租户检查。
  6. 结构化结果和错误。
  7. 必要的审计记录。

按风险逐步增加工具:

阶段工具示例策略
1计算器、静态知识查询自动执行
2只读数据库查询、搜索白名单、限流、结果截断
3创建草稿、准备变更执行后仍需用户确认发布
4修改数据、发消息、付款执行前明确确认,使用幂等键
5shell、任意文件或网络访问放进强隔离环境,默认拒绝

工具失败时返回失败,不要把错误包装成看似成功的文本。这样模型才有机会修正参数或向用户解释。

建议的学习项目

做一个“Vue Agent 工作台”,但按六个里程碑推进。

里程碑 1:纯前端假流

  • 用定时器或固定事件数组模拟文本增量。
  • 完成消息列表、停止、重试和状态展示。
  • 不接模型,不处理密钥。

你会先解决最容易被低估的流式 UI 问题。

里程碑 2:后端代理 + 单模型

  • 后端保存一个供应商密钥。
  • 前端只调用同源 /api/agent/run
  • 只支持一个模型、无工具、有限输入长度。
  • 加入认证、限流、超时和错误映射。

里程碑 3:会话与恢复

  • 消息使用稳定 ID。
  • 记录 parentId,让分支、重试和回放有明确语义。
  • 保存模型、消息和完成原因。
  • 支持刷新恢复、取消后继续和失败重试。
  • 限制本地缓存大小,图片不要无限转成 base64 保存。

里程碑 4:第一个只读工具

  • 选择可预测、低风险的工具。
  • 对输入和输出做 schema 校验。
  • 在 UI 中展示“建议调用 → 执行中 → 结果”。
  • 为未知工具和非法参数编写失败路径。

里程碑 5:需要确认的写工具

  • 把“模型建议”和“真正执行”分成两个状态。
  • 确认框展示对象、字段和不可逆影响。
  • 写操作加入幂等键,避免网络重试造成重复动作。

里程碑 6:评测与可观测性

  • 记录延迟、停止原因、工具成功率和 token 用量。
  • 对日志中的提示词、工具参数和凭据做脱敏。
  • 建立固定任务集,比较改提示词、换模型或改工具后的结果。

测试顺序

Agent 应用不能只靠真实模型手测。推荐从确定性测试开始:

  1. 状态 reducer 测试:给定事件序列,断言最终消息与运行状态。
  2. 组件测试:模拟文本流、取消、工具确认和错误。
  3. 协议测试:验证拆包、半包、断线和非法 JSON。
  4. 工具契约测试:验证 schema、权限、超时、幂等和错误返回。
  5. 端到端测试:使用假供应商跑完整浏览器流程。
  6. 真实供应商冒烟测试:少量、显式启用,不作为每次提交的默认测试。
  7. 评测集:验证结果质量,而不只是代码是否执行。

Pi 上游提供可脚本化的 faux provider,适合构造无需网络和密钥的流式事件。即使不用它,也应自己保留一个确定性的 fake transport。

常见误区

误区更好的做法
先做一个“万能 Agent”先做好一个窄任务和完整失败路径
只维护一个 loading 状态使用明确的运行状态机
把供应商 Key 放进 VITE_*通过受控后端代理调用
在浏览器运行高权限工具把执行放到后端或隔离环境
直接执行模型生成的函数名和参数工具白名单 + schema 校验
只处理 text_delta处理完成原因、错误、取消和工具事件
所有消息都塞进 localStorage设计容量、版本和迁移策略
一开始就上多模型、多工具、多 Agent单模型、无工具跑通后逐层增加
依赖真实模型写单元测试使用确定性的事件脚本和假供应商
把模型回复当作任务完成用业务断言、工具结果和评测验证

推荐的代码组织

text
src/
└── features/
    └── agent/
        ├── api/
        │   ├── agent-client.ts
        │   └── event-decoder.ts
        ├── components/
        │   ├── AgentComposer.vue
        │   ├── AgentDiffViewer.vue
        │   ├── AgentMessageList.vue
        │   ├── AgentRunStatus.vue
        │   └── ToolApprovalCard.vue
        ├── composables/
        │   └── useAgentRun.ts
        ├── model/
        │   ├── events.ts
        │   ├── messages.ts
        │   └── run-state.ts
        └── stores/
            └── agent-session.ts

按功能组织比把所有组件、stores 和 types 分散到全局目录更容易维护边界。

给前端工程师的优先级

学习顺序建议是:

  1. 流式协议与取消。
  2. 消息和运行状态建模。
  3. 模型上下文与 token 预算。
  4. 工具 schema、执行循环和确认。
  5. 后端认证、限流与隔离。
  6. 可观测性与评测。
  7. 最后才是多 Agent 编排。

Agent 产品的护城河通常不是“接入了哪个模型”,而是可靠的上下文、工具、反馈、权限和评测闭环。

下一步:

非官方中文工程指南,内容以 Pi 上游文档与源码为准。