前端工程师的 Agent 开发路线
前端工程师进入 Agent 开发,最大的优势不是“会做聊天框”,而是已经熟悉状态、异步交互、错误恢复和用户反馈。真正需要补上的,是模型流、工具调用、安全边界和评测。
这条路线的目标不是一步做出全自动 Agent,而是先做出一个状态可解释、操作可中止、结果可验证的 Agent 产品。
先建立正确的心智模型
普通聊天应用通常只有“请求 → 文本回复”。Agent 多了一个循环:
用户输入
↓
模型判断下一步
├── 直接回复 ────────────────┐
└── 请求调用工具 │
↓ │
应用校验并执行工具 │
↓ │
把工具结果交还模型 ───────┘
↓
最终回复或继续循环模型产生的工具调用只是一个提议。应用仍然负责参数校验、权限判断、执行、超时、重试、审计和用户确认。
Pi 的包应该怎么选
Pi 上游是一个 TypeScript monorepo。不同包服务于不同层,不要把它们全部装进 Vue 应用。
| 包 | 适合做什么 | 前端项目中的建议 |
|---|---|---|
@earendil-works/pi-ai | 模型目录、供应商、消息、流式响应和工具 schema | 从这里开始;生产调用优先放在后端 |
@earendil-works/pi-agent-core | Agent 循环、状态、工具执行和事件 | 需要自动工具循环时再加入 |
@earendil-works/pi-coding-agent | Pi CLI、终端会话、文件和 shell 工具 | 不要直接打进浏览器 |
@earendil-works/pi-tui | 终端 UI 组件 | 它不是 DOM/Vue 组件库 |
@earendil-works/pi-server | 实验性服务端包 | 不要把生产架构建立在未稳定 API 上 |
一个好原则是:能用 pi-ai 解决,就先不要引入完整 Agent 循环;能用一个供应商,就不要导入所有供应商。
当前 API 使用 provider collection,而不是旧的全局 getModel() / stream() 风格:
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 最小集成。
推荐的第一版架构
第一版不要让浏览器直接连接模型供应商:
Vue 3
├── 消息列表
├── 输入与停止按钮
├── 运行状态机
└── 流事件解析
│
│ 只调用你自己的同源 API
▼
应用后端
├── 用户认证与限流
├── 模型、参数和工具白名单
├── pi-ai / pi-agent-core
├── 供应商密钥
└── 工具执行器
│
▼
模型供应商与受控外部服务为什么要有后端
VITE_*环境变量会进入客户端 bundle,不能保存密钥。- 浏览器存储会被同源脚本读取,不能当作供应商密钥保险箱。
- 工具执行需要服务端权限、超时、审计和资源限制。
- 后端可以统一处理供应商切换、限流、成本和错误格式。
后端不一定复杂。第一版只需要一个经过认证的流式接口、一个模型白名单和零个工具。
用 Vue 设计 Agent 状态
不要只用 loading: boolean。先把整次运行生命周期和每个工具调用拆开:
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
建议按复杂度升级:
- 单页面、单会话:先用
useAgentRun()composable 和ref。 - 多组件共享同一会话:再引入 Pinia。
- 多会话、队列、跨路由恢复:把可序列化会话状态放进 Pinia。
AbortController、网络连接和计时器留在 composable/service 中,不要持久化。
不要因为“技术栈要完整”就提前加入 Router、Pinia 和复杂状态框架。
正确处理流式事件
流式响应不只是文本增量。你还会遇到开始、思考内容、工具调用、完成、取消和错误事件。
实现时注意:
- 按消息 ID 和内容块索引归并事件。
- 不同内容块可能交错到达,不要假设所有
text_delta连续。 - 收到错误或取消后,保留已经到达的部分内容。
- 区分
stop、length、toolUse、error和aborted。 length表示达到输出上限,不等于完整成功。- 工具参数的流式片段可能不是合法 JSON;等完整事件后再解析。
推荐把网络事件先转换成应用自己的稳定事件,再交给 Vue:
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 命令。
每个工具都应该有:
- 稳定且受控的工具名称。
- 明确的输入 schema。
- 服务端参数校验。
- 超时、取消和输出大小限制。
- 权限与租户检查。
- 结构化结果和错误。
- 必要的审计记录。
按风险逐步增加工具:
| 阶段 | 工具示例 | 策略 |
|---|---|---|
| 1 | 计算器、静态知识查询 | 自动执行 |
| 2 | 只读数据库查询、搜索 | 白名单、限流、结果截断 |
| 3 | 创建草稿、准备变更 | 执行后仍需用户确认发布 |
| 4 | 修改数据、发消息、付款 | 执行前明确确认,使用幂等键 |
| 5 | shell、任意文件或网络访问 | 放进强隔离环境,默认拒绝 |
工具失败时返回失败,不要把错误包装成看似成功的文本。这样模型才有机会修正参数或向用户解释。
建议的学习项目
做一个“Vue Agent 工作台”,但按六个里程碑推进。
里程碑 1:纯前端假流
- 用定时器或固定事件数组模拟文本增量。
- 完成消息列表、停止、重试和状态展示。
- 不接模型,不处理密钥。
你会先解决最容易被低估的流式 UI 问题。
里程碑 2:后端代理 + 单模型
- 后端保存一个供应商密钥。
- 前端只调用同源
/api/agent/run。 - 只支持一个模型、无工具、有限输入长度。
- 加入认证、限流、超时和错误映射。
里程碑 3:会话与恢复
- 消息使用稳定 ID。
- 记录
parentId,让分支、重试和回放有明确语义。 - 保存模型、消息和完成原因。
- 支持刷新恢复、取消后继续和失败重试。
- 限制本地缓存大小,图片不要无限转成 base64 保存。
里程碑 4:第一个只读工具
- 选择可预测、低风险的工具。
- 对输入和输出做 schema 校验。
- 在 UI 中展示“建议调用 → 执行中 → 结果”。
- 为未知工具和非法参数编写失败路径。
里程碑 5:需要确认的写工具
- 把“模型建议”和“真正执行”分成两个状态。
- 确认框展示对象、字段和不可逆影响。
- 写操作加入幂等键,避免网络重试造成重复动作。
里程碑 6:评测与可观测性
- 记录延迟、停止原因、工具成功率和 token 用量。
- 对日志中的提示词、工具参数和凭据做脱敏。
- 建立固定任务集,比较改提示词、换模型或改工具后的结果。
测试顺序
Agent 应用不能只靠真实模型手测。推荐从确定性测试开始:
- 状态 reducer 测试:给定事件序列,断言最终消息与运行状态。
- 组件测试:模拟文本流、取消、工具确认和错误。
- 协议测试:验证拆包、半包、断线和非法 JSON。
- 工具契约测试:验证 schema、权限、超时、幂等和错误返回。
- 端到端测试:使用假供应商跑完整浏览器流程。
- 真实供应商冒烟测试:少量、显式启用,不作为每次提交的默认测试。
- 评测集:验证结果质量,而不只是代码是否执行。
Pi 上游提供可脚本化的 faux provider,适合构造无需网络和密钥的流式事件。即使不用它,也应自己保留一个确定性的 fake transport。
常见误区
| 误区 | 更好的做法 |
|---|---|
| 先做一个“万能 Agent” | 先做好一个窄任务和完整失败路径 |
只维护一个 loading 状态 | 使用明确的运行状态机 |
把供应商 Key 放进 VITE_* | 通过受控后端代理调用 |
| 在浏览器运行高权限工具 | 把执行放到后端或隔离环境 |
| 直接执行模型生成的函数名和参数 | 工具白名单 + schema 校验 |
只处理 text_delta | 处理完成原因、错误、取消和工具事件 |
| 所有消息都塞进 localStorage | 设计容量、版本和迁移策略 |
| 一开始就上多模型、多工具、多 Agent | 单模型、无工具跑通后逐层增加 |
| 依赖真实模型写单元测试 | 使用确定性的事件脚本和假供应商 |
| 把模型回复当作任务完成 | 用业务断言、工具结果和评测验证 |
推荐的代码组织
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 分散到全局目录更容易维护边界。
给前端工程师的优先级
学习顺序建议是:
- 流式协议与取消。
- 消息和运行状态建模。
- 模型上下文与 token 预算。
- 工具 schema、执行循环和确认。
- 后端认证、限流与隔离。
- 可观测性与评测。
- 最后才是多 Agent 编排。
Agent 产品的护城河通常不是“接入了哪个模型”,而是可靠的上下文、工具、反馈、权限和评测闭环。
下一步:
- 用自动化与程序集成选定进程边界。
- 动手实现 SDK 与 Vue 最小集成。
- 用 Skill 固化开发流程,或用 Extension 扩展 Pi CLI。
- 先读权限与安全边界。
- 了解
pi-ai。 - 需要工具循环时再读
pi-agent-core。 - 研究进程集成时查看 Pi RPC 文档。