实现步骤
第一步:准备课程资料
课程资料放在:
apps/study-agent/src/data/course-materials.ts每条资料包含:
idtitletopiccontent
这让 Tool 可以用稳定字段搜索和返回摘要。
第二步:实现搜索工具
searchCourseTool 的职责只有一个:根据 query 返回相关资料。
设计重点:
- 输入 schema:
query和topK。 - 输出 schema:命中资料数组。
- 空 query 返回空数组。
- 不调用外部网络。
第三步:实现练习题工具
generateExerciseTool 根据 topic 和 difficulty 生成练习题。教学版用模板生成,真实项目可以让模型生成,但仍建议保留输出结构。
第四步:实现学习计划工具
createStudyPlanTool 把目标、天数和资料片段变成每天安排。它是确定性逻辑,因此放在 Tool 里比放在 prompt 里更容易测试。
第五步:组装 Agent
studyAgent 负责对话和工具选择。instructions 要明确:
- 先搜索课程资料。
- 不知道就说明缺少资料。
- 输出要面向初学者。
- 需要计划时调用计划工具。
- 需要练习时调用练习工具。
第六步:组装 Workflow
studyPlanWorkflow 固定执行:
flowchart LR
A[输入 goal + days] --> B[normalizeGoal]
B --> C[retrieveMaterials]
C --> D[buildPlan]
D --> E[输出计划]第七步:注册 Mastra
src/mastra/index.ts 统一注册:
agentstoolsworkflowsstorage
Mastra Studio 能否看到这些模块,主要取决于这里是否注册正确。
第八步:补上结构化输出边界
当前教学项目中,学习计划和练习题由 Tool / Workflow 返回结构化数据,而不是让 Agent 自由输出 JSON。这样做有两个好处:
- 离线模式可以运行同一套业务结构。
- 真实模型只负责解释、选择工具和组织回答,核心对象由 TypeScript 代码产生。
如果后续要让模型生成更开放的结构化结果,可以在 .generate() 中加入 structuredOutput.schema。但要额外验证工具调用是否仍然正常,并用 Zod 对 response.object 做二次校验。
第九步:设计安全和人工确认
Study Agent 默认只有读取本地资料、生成计划、生成练习题,不执行破坏性操作,因此不默认启用 approval。
一旦增加这些功能,就应该先设计人工确认:
| 新能力 | 风险 | 推荐机制 |
|---|---|---|
| 写入日历 | 修改外部系统 | Tool requireApproval |
| 发送学习报告邮件 | 对外发送内容 | Tool requireApproval |
| 删除学习记录 | 不可逆操作 | Tool requireApproval + 明确拒绝分支 |
| 购买课程或调用付费 API | 成本风险 | Approval + 成本 guardrail |
第十步:准备评估样例
最终项目至少应保留一组手工或脚本化验收样例:
| 样例 | 期望 |
|---|---|
用 3 天理解 Mastra Agent、Tool 和 Workflow | 返回 3 天计划,并引用相关资料 |
给我 Workflow 练习题 | 调用练习工具,topic 和题目匹配 |
继续上一轮计划 | 同一 memory thread 下能延续上下文 |
问一个课程资料外的问题 | 明确说明资料不足,不编造 |
这组样例未来可以迁移到 Mastra Scorers;当前先用人工检查和离线脚本保证核心行为稳定。
本项目提供了可运行版本:
npm run final:eval它把样例拆成 toolUseCorrectness、topicCoverage、expectedSignals、forbiddenSignals 和结构完整性等 scorer。后续接真实模型时,先保留这批确定性 scorer,再逐步加入 LLM judge。
第十一步:规划 RequestContext 和 Streaming
当前最终项目没有前端和多用户权限,因此没有默认实现 RequestContext。但真实应用应提前设计这些请求级变量:
| 变量 | 用途 | 不应做什么 |
|---|---|---|
user-tier | 控制可用工具、模型和 RAG topK | 不写入长期 Memory |
language | 切换回答语言和资料偏好 | 不把完整请求头交给模型 |
tenant-id | 数据隔离和工具配置 | 不暴露给 prompt |
debug | 是否返回 stream 事件细节 | 不给普通用户显示内部事件 |
前端接入时建议优先使用 .stream():
- 正常 UI 消费
textStream。 - 调试 UI 消费
fullStream,显示tool-call、tool-result和finish。 - Workflow 页面消费 workflow stream 事件,显示 step 进度和失败位置。
第十二步:预留模型策略和服务化边界
最终项目保持最小可运行,但正式应用需要提前决定:
| 设计点 | 教学版处理 | 扩展方向 |
|---|---|---|
| 模型选择 | MASTRA_MODEL 一个默认值 | 增加 fast / reasoning / fallback 策略 |
| embedding | 教学版关键词检索 | 接 ModelRouterEmbeddingModel 和向量库 |
| 前端接入 | Studio 调试 | 使用 Mastra Server endpoint 或 Client SDK |
| 鉴权 | 不默认实现 | middleware 映射 resourceId |
| 自定义 API | 不默认实现 | 只补健康检查、导出、业务聚合接口 |
这一步不急着写代码。更重要的是先列清楚「哪些由 Agent 负责,哪些由服务端负责,哪些由前端负责」。否则 vibe coding 很容易把同一段业务逻辑生成到多个地方。