不止是聊天框:我用 EdgeOne Makers Agents 给烹饪 APP 加了一位能“改菜谱”的 AI 主厨

不止是聊天框:我用 EdgeOne Makers Agents 给烹饪 APP 加了一位能"改菜谱"的 AI 主厨

做烹饪 APP 时,我一直觉得传统菜谱有一个明显短板:它能告诉用户"标准做法",却很难处理做饭现场的临时变化。

例如用户正在做辣椒炒肉,突然想问:"我想让它有一点汤汁,但又不想变成水煮口感,应该怎么做?"普通聊天机器人可以给出一段建议,但回答看完就结束了。用户还要自己记住什么时候加水、加多少、用什么火候,再切回烹饪步骤照着操作。

我想做的不是在页面右下角塞一个通用聊天框,而是让 AI 真正理解当前菜谱、当前步骤,并把有用的建议写回烹饪流程。基于这个目标,我给自己的 Vibe Cook 烹饪 APP 接入了 EdgeOne Makers Agents,做出了一位"懂菜谱,也能参与菜谱"的 AI 主厨。

本文会完整复盘这个功能的设计、接入、流式输出、结构化回写,以及我实际踩过的几个坑。

一、为什么我没有做一个普通的 AI 对话框

这个功能最初只有一句需求:在菜谱详情页和烹饪页都能问 AI。

真正实现时,我把它拆成了三个层次:

  1. 读懂上下文:Agent 需要知道菜名、份数、原料、工具、全部步骤,以及用户当前做到第几步。
  2. 给出完整解释:用户问的是烹饪问题,回答必须讲清结论、原理、用量、火候、顺序和失败风险,不能只返回一句"加一点水"。
  3. 生成可执行改动:AI 需要指出建议应该挂到原菜谱的哪一步,用户确认后才能加入页面,而不是直接篡改原菜谱。

这三个层次分别对应"对话上下文""面向用户的 Markdown 回答"和"面向程序的结构化建议"。如果只让模型自由输出一段文字,第三步会非常难做:前端不知道哪句话值得保存,也不知道它属于哪一个步骤。

因此,我最终设计的交互是:AI 先像主厨一样解释,再在回答下方生成"建议加入步骤"的卡片。用户点击"添加到此步骤"后,提示才会出现在详情页和烹饪页中,并带有 AI 标记;如果不再需要,还可以一键"清空 AI 标记"。

二、把 Agent 放进已有 Next.js 项目

我的项目是一个已有的 Next.js 烹饪应用,而不是从 Agent 模板重新起一个站点。Makers Agents 使用文件即路由约定,所以我在项目中增加了下面的结构:

bash 复制代码
frontend/
├── agents/
│   └── recipe-chef/
│       └── index.ts
├── src/
│   ├── components/recipe-agent.tsx
│   ├── lib/recipe-agent.ts
│   └── store/user-settings-store.ts
└── edgeone.json

edgeone.json 用来声明 Agent 目录、框架和运行时间:

json 复制代码
{
  "agents": {
    "framework": "openai-agents-sdk",
    "dir": "agents",
    "timeout": 300,
    "sandbox": {
      "timeout": 300
    }
  }
}

Agent 入口导出 onRequest(context)。平台通过 context 注入请求与环境变量,因此模型密钥只存在服务端,不会暴露给浏览器。当前项目使用 Makers Models 中的 @makers/deepseek-v4-flash,浏览器只访问业务路由 /recipe-chef

这一点很重要:Makers 本地开发和生产部署都是 Web 与 Agent 同项目、同入口。前端不应该请求 Agent worker、可观测面板或者 Next.js 内部端口。我的客户端最终保持同源调用:

arduino 复制代码
function getAgentEndpoint(): string {
  const configured = process.env.NEXT_PUBLIC_RECIPE_AGENT_URL?.replace(//$/, "");
  return configured || "/recipe-chef";
}

只有 Web 与 Agent 确实部署在不同域名时,才配置 NEXT_PUBLIC_RECIPE_AGENT_URL。同域部署不但配置更少,也天然避开了大部分 CORS 问题。

三、让每次提问都带上"正在做的这道菜"

用户在详情页提问时,我会把菜谱上下文随请求发送给 Agent:

css 复制代码
body: JSON.stringify({
  recipe,
  messages: messages.slice(-10),
  currentStepIndex,
})

这里的 recipe 不只是标题,还包括份数、原料、工具、步骤原文、时长和已有提示。进入烹饪模式后,还会携带 currentStepIndex。这样,同样一句"现在该怎么办",在详情页和炒制到第三步时可以得到不同回答。

平台用 Makers-Conversation-Id 标识连续会话。我按菜谱在 sessionStorage 中保存一个合法的会话 ID,同一道菜继续追问时复用它:

javascript 复制代码
const conversationId = `cook_${crypto.randomUUID()
  .replace(/-/g, "")
  .slice(0, 31)}`;
​
await fetch("/recipe-chef", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Makers-Conversation-Id": conversationId,
  },
  body: JSON.stringify(payload),
});

四、一份回答,同时服务用户和页面

这是整个功能里我认为最值得复用的设计。

如果要求模型只输出 JSON,页面虽然容易更新,但用户会得到一份机械、很短的答复;如果只输出 Markdown,用户读起来舒服,程序却无法可靠地把建议挂到步骤上。

我的解决办法是定义一个简单的双段输出协议:

css 复制代码
完整 Markdown 主厨解答
​
@@VIBE_COOK_SUGGESTIONS@@
[{"stepIndex":2,"reason":"汤汁需要在合炒阶段形成", "tips":["沿锅边加入......"]}]

系统提示词要求正文通常包含 400~700 个中文字,先给结论,再解释原理、操作顺序、口感变化和风险;分隔符后则必须输出 JSON 数组。每条建议只能关联已有步骤,每步最多三条 tip,不能新造步骤,也不能声称已经替用户修改页面。

服务端在流式读取模型输出时,会暂存一小段字符以识别分隔符。分隔符之前的内容立即作为 delta 发给前端,之后的 JSON 留在服务端解析。最终浏览器只会收到两类核心事件:

vbnet 复制代码
event: delta
data: {"text":"......"}
​
event: complete
data: {"suggestions":[......]}

这样既不会把内部 JSON 闪到聊天界面里,又能在回答结束后生成结构化操作卡片。

五、流式输出不是加一个 stream: true 就结束了

为了让回答不是长时间停在"思考中",我在模型请求中开启了流式输出:

css 复制代码
body: JSON.stringify({
  model: "@makers/deepseek-v4-flash",
  temperature: 0.7,
  stream: true,
  messages,
})

Agent 读取上游 OpenAI 兼容 SSE,提取 choices[0].delta.content,再转换成自己的 SSE 协议。前端使用 ReadableStreamTextDecoder 增量解析,每收到一个 delta 就更新同一条 assistant 消息;收到 complete 后,再把建议卡片挂到这条回答上。

Markdown 渲染使用 react-markdownremark-gfm。标题、列表、粗体和具体用量可以边生成边显示,烹饪建议不再是一堵纯文本。

这里还有一个细节:分隔符本身可能跨越多个网络分片。如果每收到一段就立刻原样推给前端,用户可能短暂看到半截 @@VIBE...。所以服务端始终保留不超过分隔符长度的一段缓冲区,只有确认不属于标记时才向外发送。

六、AI 可以建议,但最终决定权必须交给用户

我没有让 Agent 直接修改菜谱。模型输出毕竟存在不确定性,尤其是火候、咸度和液体用量,用户应该先看到建议再决定是否采用。

用户点击"添加到此步骤"后,前端才会调用:

scss 复制代码
addRecipeTips(recipe.id, suggestion.stepIndex, suggestion.tips);

这些内容按 recipeId -> stepIndex -> tips 存入 Zustand,并通过持久化中间件保存到本地。详情页会在对应步骤下显示 AI 提示;烹饪页只显示当前步骤相关的提示。重复内容会去重,已经应用过的建议卡片也会显示为"已添加到此步骤"。

功能上线前我又补了"清空 AI 标记"。这看起来只是一个小按钮,却解决了一个真实问题:AI 建议属于用户当次的个性化调整,不应永久污染原始菜谱。用户可以随时回到标准做法。

七、实操中最容易踩的坑

不同模型的参数约束并不相同

我曾切换模型测试,遇到过模型只接受特定 temperature 的 400 错误。这提醒我:兼容 OpenAI 请求格式,不代表所有采样参数也完全相同。切换模型时,不能只改模型名,还要检查供应方对 temperature、最大输出和流式格式的约束。

目前项目改用 @makers/deepseek-v4-flash,并把模型名放在服务端环境变量中,前端无需改动。这个设计让替换模型不会影响页面协议。

八、本地调试与部署

项目关联 Makers 后,本地调试流程是:

bash 复制代码
npm install -g edgeone
edgeone makers link
edgeone makers dev

本地请求要携带合法的会话 ID。除了在页面中测试,我还会直接用 curl -N 请求 /recipe-chef,确认响应中持续出现 event: delta,并最终以带有建议数组的 event: complete 结束。这样可以快速区分是页面渲染问题、SSE 解析问题,还是 Agent 上游调用问题。

生产环境可以把代码推送到已导入 EdgeOne Makers 的远程仓库,让生产分支提交自动触发构建;也可以使用 CLI:

xml 复制代码
edgeone makers deploy -n <project-name>

项目演示地址:https://cook.corerevive.cn/recipe/meat_dish_la-jiao-chao-rou

九、实际效果与我的思考

接入前,Vibe Cook 展示的是一份固定菜谱;接入后,它能围绕同一份菜谱生成用户自己的"现场版本"。以辣椒炒肉为例,用户提出"想要一点汤汁"后,Agent 不只是解释做法,还能把"何时加入液体、用什么火候收汁、如何避免青椒出水"等提示准确挂到合炒步骤。用户继续做菜时,不需要回聊天记录里翻答案。

这次实践让我重新理解了"在已有业务中接入 Agent"。真正有价值的并不是多了一个 AI 入口,而是让自然语言最终落到产品已有的数据结构和操作流程里:

  • 对用户,Agent 是一位能结合当前菜谱回答问题的主厨;
  • 对页面,Agent 是一个生成结构化步骤建议的能力;
  • 对开发者,Makers 把 Agent 路由、模型接入、本地调试和部署放回同一个项目,减少了额外服务之间的拼装。

后续我计划继续扩展两类能力:一是把用户的忌口、口味与厨具条件作为长期偏好,让建议更个性化;二是让 Agent 联动现有的份数与采购换算功能,例如用户输入"家里只有 150 克猪肉",页面自动换算其他原料后,再由 Agent 补充小份量烹饪时的火候变化。

对我来说,这个功能已经从"给菜谱加一个聊天框",变成了"让每个人都能拥有一份可调整、可执行的菜谱"。这也是我认为 Agent 最适合进入垂直产品的方式:不悬浮在业务之外,而是深入用户正在完成的那件事。

相关推荐
咩咩啃树皮11 小时前
第32篇:前端本地存储全解——Cookie、localStorage、sessionStorage 区别与实战
前端
腻害兔13 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 框架层:15 个 Starter 到底在干什么?
前端·vue.js·产品经理·ai编程
程序员黑豆14 小时前
从零开始:编写第一个鸿蒙(HarmonyOS)程序
前端·harmonyos
lastknight15 小时前
CSS @layer
前端·css
李宸净15 小时前
Web自动化测试selenium+python
前端·python·selenium
淡海水15 小时前
06-04-YooAsset源码-Unity加密解密服务
前端·unity·性能优化·c#·游戏引擎·yooasset
trigger33316 小时前
desk-health-web-community-post
前端
倒流时光三十年16 小时前
Logback 系列(7):常用 Appender(控制台 / 文件 / 滚动文件)
java·前端·logback