Skip to content

使用规范定义的标记语言(例如YAML)来定义每一个菜谱 #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
zgldh opened this issue Feb 18, 2022 · 49 comments
Open
Labels
open discussion Discussion for long-term enhancement

Comments

@zgldh
Copy link

zgldh commented Feb 18, 2022

使用规范定义的标记语言、JSON 或 yaml 来定义每一个菜谱。
有利于使用程序进行深加工,挖掘菜谱更多价值。

@JasonChen233
Copy link

再开发一下配套的程序和机器人,输入yaml就能做出菜来了

@Anduin2017
Copy link
Owner

其实我觉得不需要设计新语言(这太难了),而可以在现有语言上开发一个库。例如用JavaScript描述做菜流程,每个原料都是对象,可以处理原料

@zgldh
Copy link
Author

zgldh commented Feb 18, 2022

其实我觉得不需要设计新语言(这太难了),而可以在现有语言上开发一个库。例如用JavaScript描述做菜流程,每个原料都是对象,可以处理原料

最好是以配置的形式来做,比如 Dockerfile 这种。
如果是编程语言的库,那灵活性太大了,反而不可预知后果。

@recolic
Copy link
Collaborator

recolic commented Feb 19, 2022

用code来表示recipe,然后code可以直接转换成人类可读的菜谱,或机器可读的自动炒菜程序。

我目前能看到的好处是:

  1. 菜谱编写者可以更容易标准化菜谱。
  2. 自动补全和高亮能帮助菜谱编写者加快效率。

@recolic
Copy link
Collaborator

recolic commented Feb 19, 2022

前几天我在思考,如果有一种语言用来表示中餐食谱,它需要哪些基本语法。

然后我意识到,使用json、xml、yaml这类标记语言,很可能导致语法过度繁琐(他们都是设计用于处理很general的问题的)。这可能会打击贡献菜谱的厨师的积极性。这种语法最好写起来非常方便。

然后我考虑过python、js之类的现有脚本语言,语言的现有语法、灵活性似乎也会对编写者造成困扰。

我写了一个类似这样的sample,但最后想了想又感觉,相比于直接写markdown来说没有太大的优势。(毕竟现在没有家用做饭机器人)
Screenshot from 2022-02-19 18-30-55

@zgldh
Copy link
Author

zgldh commented Feb 19, 2022

我倾向于学习 Dockerfile 的做法。

From 引入别的菜谱
Material 定义原料
Kitchenware 定义炊具

Put 将原料放入炊具

Chopping 砧板工序(砧板)
Steam 蒸炖工序(上什)
Fry 烹炒工序(炉头)
Paoding 屠宰分解(水台)
Seasoning 料头预加工、装饰(打荷)
Brine 烧蜡卤味工序(烧腊)
Dimsum 点心面食工序(点心)
以上工序参考自这里

Export 将某炊具内容物导出为一个产物,产物可以被其他菜谱引用。也可以直接吃。如豆瓣酱、回锅肉都可以是产物。

抛砖引玉,有别的想法也说说。

@520MianXiangDuiXiang520
Copy link

520MianXiangDuiXiang520 commented Feb 20, 2022

是否可以将具体的做菜流程拆分成多个不同的步骤(Step),每个 Step 由一个或多个子 Step 或不可拆分的原子步骤(AtomStep)组成,通过定义触发器(Tigger)之类的组件应该可以描述整个流程。同时可以最大程度的保证每个步骤的清晰性,如 #55 的流程可以表示为:

Step1:
    Input: 番茄
    Handle:
        Step1: 
            Input: 番茄
            Output: $11
            Handle: 开水烫番茄表皮
        Step2: 
            Input: $11
            Output: $12
            Handle: $1 放入冷水
        Step3:
            Input: $12
            Output: $13
            Handle: 去掉 $12 外皮
    Output: $13
Step2:
    Input: $13
    Output: $2
    Handle: $13 切块
Step3:
    Input: 鸡蛋,盐
    Output: $3
    Handle:
        Step1:
            Input: 鸡蛋
            Output: $31
            Handle: 鸡蛋打入碗中
        Step2:
            Input: $31
            Output: $32
            Handle: 加入 人数 * 1g 盐
        Step3:
            Input: $32
            Output: $33
            Handle: 搅拌均匀
Step4:
    Input: 油 油数量为 鸡蛋数量 * 4 ml
    Output: $4
    Handle: 起锅烧油
Step5:
    Input: $4
    Trigger: $4 冒烟
    Output: $5
    Handle: 
        - 往 $4 中加入  $3 
        - 等待 10s 或两面金黄
 // ……

单纯整活

@myuanz
Copy link

myuanz commented Feb 21, 2022

已经有人做过了, 而且连APP都有了

https://cooklang.org/

Cooklang and the tools we’ve built to use it, you can:

simplify your personal recipe management;
streamline your shopping routine;
make cooking more fun.
Here’s how the Cooklang ecosystem makes that happen:

All recipes are human-readable text files.
Everything is a file. No databases. And you have complete control over your information.
All the tools are simple, focused, and efficient; the UNIX way.

@c4fun
Copy link

c4fun commented Feb 21, 2022

应该是recipeAsCode吧?

比如pipelineAsCode=gitops;recipeAsCode就是cookOps

@zgldh zgldh changed the title 我们需要 Code as Recipe! 我们需要 Recipe as Code! Feb 21, 2022
@Anduin2017 Anduin2017 pinned this issue Feb 21, 2022
@hax
Copy link

hax commented Feb 21, 2022

Cooklang 没有本地化啊,我们需要中文编程。

@Fjaxzhy
Copy link

Fjaxzhy commented Feb 21, 2022

Cooklang 没有本地化啊,我们需要中文编程。

可以的话甚至可以来点可视化(

@myuanz
Copy link

myuanz commented Feb 21, 2022

Cooklang 没有本地化啊,我们需要中文编程。

image

菜谱怎么会跟自然语言强绑定呢?

>> 中文测试: 张三☺️

@西红柿{2个}洗净。去蒂,切成边长不超过 4cm 的小块
将@鸡蛋{2~3个}打入碗中,加入@盐{2%g},搅匀
热锅,加@油{8%ml}
(可选)加入@饮用水{300%ml},并加热 30 秒(这可以避免最终盛盘后可能的汤汁不足)

@zgldh
Copy link
Author

zgldh commented Feb 21, 2022

Cooklang 功能太弱了,只有食材提取的能力。
更关键的烹饪技法没有规范化。

@shinobuffer
Copy link
Contributor

还是开发一个食谱编辑器靠谱,参考 #135

@xumingkuan
Copy link
Contributor

https://www.dangermouse.net/esoteric/chef.html 这个语言的烹饪技法倒是规范化了,但是种类有点少,而且只有英语。
(其实这个语言根本不是 Recipe as Code 而是 Code as Recipe)

@baby5
Copy link

baby5 commented Feb 22, 2022

接入语音AI就可以当厨房助手了

@Anduin2017 Anduin2017 changed the title 我们需要 Recipe as Code! 使用规范定义的标记语言(例如YAML)来定义每一个菜谱 Feb 22, 2022
@bibab0
Copy link

bibab0 commented Feb 25, 2022

分享一个做菜,你们也太卷了吧

@amitbha
Copy link

amitbha commented Feb 25, 2022

分享一个做菜,你们也太卷了吧

这叫互联网思维啊
我觉得菜谱语言需要支持数量计算,不然得凭经验,对新手不友好
经验部分也能数字化

要不使用扩展 markdown 语法描述好了,还能转成思维导图

@momoblydblk
Copy link

感觉食材的处理方式都可以被枚举吧……当成一个class来处理的可能性也是不小的
猪肉 pork1 = new 猪肉( 100g, 五花肉);
pork1.切条(手指粗细);
锅 wok1 = new 锅(炒锅);
wok1.add(油, 1茶勺);
wok1.heat(100摄氏度);
wok1.add(pork);

@c4fun
Copy link

c4fun commented Mar 6, 2022

Cooklang 功能太弱了,只有食材提取的能力。 更关键的烹饪技法没有规范化。

其实我觉得Cooklang只能够作为定义中的食材提取的部分;而这个烹饪技法--如同大家所说--可以用github action或者gitlab-ci等方式来完成。

而烹饪技法的流水线,又可以进一步提高人类协作的效率,可能这是目前的唯一价值;不然厨师们为啥要用一个不熟悉的语言来定义菜谱呢?

当然最终目标还是输入菜谱之后,锅里面就能够标准化的生产出对应的菜,不过那个时候估计人工智能已经很发达了,还要喂养人类干嘛,当电池吗:)

@SekiBetu
Copy link

SekiBetu commented Mar 9, 2022

toml更加易读

@JiYouMCC
Copy link

JiYouMCC commented Mar 22, 2022

image
也是整活儿。。。

@SWYZZWH
Copy link

SWYZZWH commented Mar 22, 2022

用顺序、条件、循环而不是静态配置表示做菜过程更自然。为了让菜谱更标准、可复制,可以把炊具、食材、操作者先做抽象,暴露出一系列API。比如微波炉实现 heater 接口,西红柿实现 cutable 和 isPeeled 接口,操作者可以是人或机器,实现类似 cutter 接口。基于这些抽象就可以写出标准的执行程序,具体使用哪种编程语言不关键。为了让菜谱更好理解,可以利用低代码方式比如可视化编程

@samzhangjy
Copy link

感觉可以做一个通用的食谱编辑器,食谱提供者用 UI 界面录入食谱后程序自动转存为一个通用的格式(假定是数据库里),然后根据数据库里的数据可以自动生成各种格式的菜谱( Markdown , YAML 等 )。这样第三方的就程序可以自己读取转换后的产物或源数据。

@wace610
Copy link

wace610 commented Mar 29, 2022

可以考虑NLP+流程挖掘,使用BPMN或者Petri网表示菜谱,进而进行多模态转换

@destroy314
Copy link

简单点可以直接用一个带时间约束和函数表示的AOE网,输入菜谱使用者的条件求得一个拓扑排序,再转为自然语言/进行代码生成

@Mi-Little
Copy link

我有个新的想法,写一个脚本, 把食谱生成一个视频来,供大家边做边看,官网可以在菜谱旁边加一个链接,就是视频,当然最主要的是 语音表达好步骤,就行不用边做边看菜谱了。

@zhangsanbin
Copy link

是否可以将具体的做菜流程拆分成多个不同的步骤(Step),每个 Step 由一个或多个子 Step 或不可拆分的原子步骤(AtomStep)组成,通过定义触发器(Tigger)之类的组件应该可以描述整个流程。同时可以最大程度的保证每个步骤的清晰性,如 #55 的流程可以表示为:

Step1:
    Input: 番茄
    Handle:
        Step1: 
            Input: 番茄
            Output: $11
            Handle: 开水烫番茄表皮
        Step2: 
            Input: $11
            Output: $12
            Handle: $1 放入冷水
        Step3:
            Input: $12
            Output: $13
            Handle: 去掉 $12 外皮
    Output: $13
Step2:
    Input: $13
    Output: $2
    Handle: $13 切块
Step3:
    Input: 鸡蛋,盐
    Output: $3
    Handle:
        Step1:
            Input: 鸡蛋
            Output: $31
            Handle: 鸡蛋打入碗中
        Step2:
            Input: $31
            Output: $32
            Handle: 加入 人数 * 1g 盐
        Step3:
            Input: $32
            Output: $33
            Handle: 搅拌均匀
Step4:
    Input: 油 油数量为 鸡蛋数量 * 4 ml
    Output: $4
    Handle: 起锅烧油
Step5:
    Input: $4
    Trigger: $4 冒烟
    Output: $5
    Handle: 
        - 往 $4 中加入  $3 
        - 等待 10s 或两面金黄
 // ……

单纯整活

支持这个做法,把每一道菜所涉及的每一个步骤、工序、节点都进行量化,加上时间轴的纬度,有利于今后机器学习和炒菜机器人的实施。然后动态生成markdown文件,有利于人类的阅读。

@K2FeO4cn
Copy link

是否可以将具体的做菜流程拆分成多个不同的步骤(Step),每个 Step 由一个或多个子 Step 或不可拆分的原子步骤(AtomStep)组成,通过定义触发器(Tigger)之类的组件应该可以描述整个流程。同时可以最大程度的保证每个步骤的清晰性,如 #55 的流程可以表示为:

Step1:
    Input: 番茄
    Handle:
        Step1: 
            Input: 番茄
            Output: $11
            Handle: 开水烫番茄表皮
        Step2: 
            Input: $11
            Output: $12
            Handle: $1 放入冷水
        Step3:
            Input: $12
            Output: $13
            Handle: 去掉 $12 外皮
    Output: $13
Step2:
    Input: $13
    Output: $2
    Handle: $13 切块
Step3:
    Input: 鸡蛋,盐
    Output: $3
    Handle:
        Step1:
            Input: 鸡蛋
            Output: $31
            Handle: 鸡蛋打入碗中
        Step2:
            Input: $31
            Output: $32
            Handle: 加入 人数 * 1g 盐
        Step3:
            Input: $32
            Output: $33
            Handle: 搅拌均匀
Step4:
    Input: 油 油数量为 鸡蛋数量 * 4 ml
    Output: $4
    Handle: 起锅烧油
Step5:
    Input: $4
    Trigger: $4 冒烟
    Output: $5
    Handle: 
        - 往 $4 中加入  $3 
        - 等待 10s 或两面金黄
 // ……

单纯整活

支持这个做法,把每一道菜所涉及的每一个步骤、工序、节点都进行量化,加上时间轴的纬度,有利于今后机器学习和炒菜机器人的实施。然后动态生成markdown文件,有利于人类的阅读。

如果用json会不会更好,yaml对于编写者的要求会更高,而且json中用一个大数组来维护步骤和原料是否对于机器更易读……?

这是修改后的 西红柿炒鸡蛋.md

{
    "head":{
        "title":"西红柿炒鸡蛋的做法",
        "description":"西红柿炒蛋是中国家常几乎最常见的一道菜肴。它的原材料易于搜集,制作步骤也较为简单,所以非常适合新厨师上手,是很多人学习做菜时做的第一道菜。"
    },
    "source":[//这里将“计算”和“必备原料和工具”合并在一起简短源码
        {
            "class":"source.tomato", //这里需要单独维护一个json来确保这个不会出现诸如“西红柿”和“番茄”的冲突,在进行构建pages的时候同时可以创建通向诸如“如何选购西红柿”的各种提示,方便后续需求,同时比如(约 180g)这种内容可以被定义在其中。
            "optional":false,
            "amount":{//这里指明每份的用量,
                "type":"piece",//按照个计算,同时"gram"是以克计算
                "count":1
            },
            "particular":[//特别指出的部分,可以对上面的内容复写,对于编译时展示的内容需要进行自行定义
                {"type":"overwrite","class":"source.tomato","key":"weight","value":"180"}//覆写西红柿的重量
            ]
        },
        //...
        {
            "class":"source.green_onion",
            "optional":true,
            "amount":{
                "type":"range",
                "left":{
                    "type":"weight",
                    "counts":0
                },
                "right":{
                    "type":"weight",
                    "counts":10
                },
            },
            "particular":[]
        }
    ],
    "step":[
        {
            "class":"source.tomato.wash",
            "optional":false,
            "particular":[]
        },//可以说是最简短的步骤(
        {
            "class":"source.tomato.skin_remove",
            "optional":true,
            "particular":[]
        },//同样的,“开水烫表皮,然后将西红柿放入冷水,剥去外皮”这句话被规范化后被定义在source.tomato.skin_remove里面
        {
            "class":"source.tomato.pedicle_remove",
            "optional":false,
            "particular":[]
        },//步骤细分,有助于机器识别
        {
            "class":"operator.cut",
            "optional":"false",
            "operation":{
                "object":"source.tomato",
                "particular":[
                    {
                        "type":"overwrite",
                        "class":"operator.cut",
                        "key":"sized",
                        "value":true
                    },
                    {
                        "type":"append",
                        "class":"operator.cut",
                        "key":"to_volume",
                        "value":"64"
                    },
                    {
                        "type":"overwrite",
                        "class":"operator.cut",
                        "key":"__volume_into_ridge_length",
                        "value":true
                    }
                ]
            },
            "particular":[
                {
                    "type":"macro",//宏
                    "class":".",//.指代本文件
                    "key":"","value":"",//宏对于固定键不兼容
                    "macro":"DEF '西红柿块' AS {.}.result"//{.}指代"particular"所在的步骤,result是抽象的产物
                }
            ]
        },
        //...
        {
            "type":"operator.plate",
            "optional":false,
            "particular":[]
        }
    ]
}

当然缺点在我改完这一点之后我感觉比较明显了……

文件大小最起码要增大一倍,对于原料的定义需要更多更多的文件,编译生成Markdown的时候需要更多更多的中间代码……好处可能是厨师机器人可以抽象的多线程(虽然会有堵塞和死锁)的做出菜……

github的markdown展示中json不支持注释...虽然说本来JSON规范也不推荐加入注释,将就看一下吧……orz

可能整了个坏活

@Xinrui-Fang
Copy link

Xinrui-Fang commented Jul 4, 2022

2021年谷歌在人机交互顶会UIST发了一篇论文可以把markdown的菜谱生成教学视频,分享在这里供大家参考:
论文链接:https://dl.acm.org/doi/10.1145/3472749.3474778
视频:https://www.youtube.com/watch?v=oq4RuqEb6Eg&list=PLqhXYFYmZ-VeKUIuttbQWomTQ-oXF6PLf&index=83

@Rickyxrc
Copy link

可以先编写几个基本类工具,食材等等,然后派生成为更详细的内容。
番茄,,这些,然后抽象出方法(对于锅来说,有,对于番茄来说有去皮,切丁等)这样在不同的解释器里可以针对相同的方法给出不同的动作。
如对于电脑,则直接显示在屏幕上,对于语音控制设备,则发出语音指示,对于(以后可能出现的)做饭机器人,则执行动作等等。
我的想法是基于现有的语言开发多套解析语法相同,但是解析结果不同的库来适配这些情况,这样,每个文件描述的是做饭的流程,也可以使用原生的import之类的功能避免重复coding。
这是我的想法最后的文件描述(我也不知道描述了什么,但是思想应该传递到了):

import { Tomato, Water } from "Cooklib/Ingredients";
import { Knife, Pot } from "Cooklib/Tools";
function MakeDish(peopleNum) {
    Water.require("size", peopleNum * 1500, peopleNum * 2000)//1500至2000ml水
        .then(() => {
            Tomato.require("size", 100, 500)//要求每个在100克到500克之间,此时做饭人或机器可以自检,通过后继续执行
                .then(() => {
                    Tomato.require("num", peopleNum * 3, peopleNum * 5)//要求3到5个
                        .then(() => {
                            Knife.cutInPieces(Tomato)
                                .then((TomatoPiece) => {
                                    Pot.Put(Water, Water.size)//将 水 放入 锅 (其实倒入会更好)
                                        .then(() => {
                                            Pot.BoilUntilTemperature(Water.boilingPoint)//将水烧到沸点(沸点可以根据当地气压测量)
                                                .then(() => {
                                                    Pot.put(TomatoPiece, 1.0)//放入100%数量的番茄碎
                                                        .then(() => {
                                                            Pot.BoilUntilTime(60 * 1000)//等待60000毫秒(一分钟)
                                                                .then(() => {
                                                                    return { "status": 0, "element": Pot.getElement() };//将锅中物品装盘备用(doge)
                                                                })
                                                        })
                                                })
                                        })
                                });
                        })
                });
        })
        .catch((err) => {
            return { "status": -1, "err": err.code };
        })
}

如果有什么更好的想法欢迎提出来。

@binsee
Copy link

binsee commented Sep 24, 2022

可以先编写几个基本类工具,食材等等,然后派生成为更详细的内容。 如番茄,,这些,然后抽象出方法(对于锅来说,有,对于番茄来说有去皮,切丁等)这样在不同的解释器里可以针对相同的方法给出不同的动作。 如对于电脑,则直接显示在屏幕上,对于语音控制设备,则发出语音指示,对于(以后可能出现的)做饭机器人,则执行动作等等。 我的想法是基于现有的语言开发多套解析语法相同,但是解析结果不同的库来适配这些情况,这样,每个文件描述的是做饭的流程,也可以使用原生的import之类的功能避免重复coding。 这是我的想法最后的文件描述(我也不知道描述了什么,但是思想应该传递到了):

import { Tomato, Water } from "Cooklib/Ingredients";
import { Knife, Pot } from "Cooklib/Tools";
function MakeDish(peopleNum) {
    Water.require("size", peopleNum * 1500, peopleNum * 2000)//1500至2000ml水
        .then(() => {
            Tomato.require("size", 100, 500)//要求每个在100克到500克之间,此时做饭人或机器可以自检,通过后继续执行
                .then(() => {
                    Tomato.require("num", peopleNum * 3, peopleNum * 5)//要求3到5个
                        .then(() => {
                            Knife.cutInPieces(Tomato)
                                .then((TomatoPiece) => {
                                    Pot.Put(Water, Water.size)//将 水 放入 锅 (其实倒入会更好)
                                        .then(() => {
                                            Pot.BoilUntilTemperature(Water.boilingPoint)//将水烧到沸点(沸点可以根据当地气压测量)
                                                .then(() => {
                                                    Pot.put(TomatoPiece, 1.0)//放入100%数量的番茄碎
                                                        .then(() => {
                                                            Pot.BoilUntilTime(60 * 1000)//等待60000毫秒(一分钟)
                                                                .then(() => {
                                                                    return { "status": 0, "element": Pot.getElement() };//将锅中物品装盘备用(doge)
                                                                })
                                                        })
                                                })
                                        })
                                });
                        })
                });
        })
        .catch((err) => {
            return { "status": -1, "err": err.code };
        })
}

如果有什么更好的想法欢迎提出来。

可怕的回调地狱,async / await 语法多好

import { Tomato, Water } from 'Cooklib/Ingredients'
import { Knife, Pot } from 'Cooklib/Tools'
async function MakeDish(peopleNum) {
  try {
    await Water.require('size', peopleNum * 1500, peopleNum * 2000) //1500至2000ml水
    await Tomato.require('size', 100, 500) //要求每个在100克到500克之间,此时做饭人或机器可以自检,通过后继续执行
    await Tomato.require('num', peopleNum * 3, peopleNum * 5) //要求3到5个
    const TomatoPiece = await Knife.cutInPieces(Tomato)
    await Pot.Put(Water, Water.size) //将 水 放入 锅 (其实倒入会更好)
    await Pot.BoilUntilTemperature(Water.boilingPoint) //将水烧到沸点(沸点可以根据当地气压测量)
    await Pot.put(TomatoPiece, 1.0) //放入100%数量的番茄碎
    await Pot.BoilUntilTime(60 * 1000) //等待60000毫秒(一分钟)
    return { status: 0, element: Pot.getElement() } //将锅中物品装盘备用(doge)
  } catch (error) {
    return { status: -1, err: err.code }
  }
}

@meowjiao321
Copy link

学习一下makefile?

@lumynou5
Copy link

是否可以将具体的做菜流程拆分成多个不同的步骤(Step),每个 Step 由一个或多个子 Step 或不可拆分的原子步骤(AtomStep)组成,通过定义触发器(Tigger)之类的组件应该可以描述整个流程。同时可以最大程度的保证每个步骤的清晰性,如 #55 的流程可以表示为:

Step1:
    Input: 番茄
    Handle:
        Step1: 
            Input: 番茄
            Output: $11
            Handle: 开水烫番茄表皮
        Step2: 
            Input: $11
            Output: $12
            Handle: $1 放入冷水
        Step3:
            Input: $12
            Output: $13
            Handle: 去掉 $12 外皮
    Output: $13
Step2:
    Input: $13
    Output: $2
    Handle: $13 切块
Step3:
    Input: 鸡蛋,盐
    Output: $3
    Handle:
        Step1:
            Input: 鸡蛋
            Output: $31
            Handle: 鸡蛋打入碗中
        Step2:
            Input: $31
            Output: $32
            Handle: 加入 人数 * 1g 盐
        Step3:
            Input: $32
            Output: $33
            Handle: 搅拌均匀
Step4:
    Input: 油 油数量为 鸡蛋数量 * 4 ml
    Output: $4
    Handle: 起锅烧油
Step5:
    Input: $4
    Trigger: $4 冒烟
    Output: $5
    Handle: 
        - 往 $4 中加入  $3 
        - 等待 10s 或两面金黄
 // ……

单纯整活

認同,但我覺得有些地方可能會讓食譜編寫及理解的難度提升:

  • StepN 的編號看起來有點冗長,我覺得可以改為 YAML 的列表語法。
  • Step1 的子步驟基本上就是將前一步驟的產物(output)作為下一步驟的原料(input),也就是 pipeline 的概念,可以簡化。冗長的 InputOutput 可能導致食譜編寫上容易出錯。
  • trigger 會使控制流程變得複雜,不方便閱讀,也無法明確表示多個 trigger 同時觸發時的順序,因此我改為上個步驟的結束條件。
  • 沒有分開的原材料列表,不利於事前準備材料。

我參考 GitHub Actions,設計了較接近自然語言風格的語法,且可以對應到現有的 Markdown 描述方式。

示例(根據 現有的「西红柿炒鸡蛋」食譜,但簡化過,例如所有食材都需要洗淨就省略了):

name: 西红柿炒鸡蛋
desc: 西红柿炒蛋是中国家常几乎最常见的一道菜肴。它的原材料易于搜集,制作步骤也较为简单,所以非常适合新厨师上手,是很多人学习做菜时做的第一道菜。
ingredients:
  - item: 番茄
    amount: 1
    unit: 
  - item: 雞蛋
    amount: 1.5
    unit: 
  - item: 食用油
    amount: 6
    unit: mL
  - item: 
    amount: 1.5-2
    unit: g
  - item: 
    amount: 0|1
    unit: mL
  - item: 
    amount: 0-2
    unit: g
  - item: 蔥花
    amount: 0-10
    unit: g
jobs:
  - dependencies:
      - 番茄
    steps:
      - optional: true
        desc: 去掉番茄的外表皮
        do:
          - 用開水燙其表皮
          - 放入冷水
          - 剝去外皮
      - do: 去蒂
      - do: 切成邊長不超過 4 cm 的塊
    artifact: 番茄塊
  - dependencies:
      - 雞蛋
      - 
      - 
    steps:
      - do: 將雞蛋打入碗中
      - do: 加入 (1 * 份數) g 的鹽
      - do: 加入醋
      - do: 攪勻
    artifact: 雞蛋液
  - dependencies:
      - 食用油
      - 雞蛋液
      - 番茄塊
    steps:
      - do: 熱鍋
      - do: 加入食用油
      - do: 等待
        until: 油熱
      - do: 加入雞蛋液
      - do: 翻炒
        until: 雞蛋結為固體且顏色微黃
      - do: 關火
      - do: 將雞蛋盛盤
      - do: 開火
      - do: 加入番茄塊
      - do: 用鍋鏟拍打並翻炒
        for:
          amount: 20
          unit: s
        until: 番茄變得軟爛
      - do: 加入剛剛盛出的雞蛋
      - do: 翻炒均勻
      - do: 加入剩餘的鹽
      - do: 加入糖
      - do: 加入蔥花
      - do: 翻炒均勻
      - do: 結束

說明:

  • name:菜品的名稱。
  • desc:菜品的描述。
  • ingredients:一份(通常正好夠一人食用)所需的食材,可按比例調整。
  • jobs:要做的事,每個 job 都有各自的產物,且每個 job 都可並行(concurrent),除非它們之間有依賴關係,例如可以同時切番茄和打蛋而不互相衝突。
  • jobs[*].dependencies:job 中涉及的食材(可能是其他 job 加工過的)。
  • jobs[*].artifact:job 輸出的產物,只能是字串,其表示一個整體或集合,例如「番茄塊」不僅指一個番茄塊。
  • jobs[*].steps:job 的步驟,是一連串對一個整體物品、集合或容器進行的操作集合。
  • jobs[*].steps[*].desc:步驟的整體描述,如果沒有子步驟不建議使用。
  • jobs[*].steps[*].do:步驟的動作,可以是一個列表來表示子步驟,應儘可能簡短。
  • jobs[*].steps[*].until:停止動作的條件。
  • jobs[*].steps[*].for:動作持續的時間。

不過還有許多問題有待改進,像盛出半熟雞蛋的地方,應該要切成兩個 job 的,但是那樣似乎無法表示先後順序。

PS:抱歉用的是繁體,希望不會造成溝通上的困難。

@tongque0
Copy link

tongque0 commented Dec 13, 2024

我创建了一个仓库 https://github.com/tongque0/HowToCook-json ,用于将菜谱转换为指定格式的json,并且每天根据当前仓库菜谱进行更新,如果有需要的菜谱格式,可以提交issue,可以加入支持,希望能够帮到大家。

@Anduin2017
Copy link
Owner

Anduin2017 commented Mar 20, 2025

三年过去了。我觉得这个issue可以复活了……毕竟我们可以用AI去将这个仓库的菜谱转换成形式语言了。我们只需要定义一套规则就可以了。

遗憾的是我认为大家提出的大部分想法最终都会陷入一个冗余问题。我读到的代码中在“冗余”和“严格”之间最好的平衡我认为是这种。

这样我们可以使用编译器去检查一道菜是否合理利用了所有原材料,并且方便的计算它的热量、原材料需求、难度、需要的工具,彻底解决“中间出现上文未提到的材料”问题和“菜谱写错了”的问题,并且真正达到这个仓库“严谨”的目标。

可以先编写几个基本类工具,食材等等,然后派生成为更详细的内容。 如番茄,,这些,然后抽象出方法(对于锅来说,有,对于番茄来说有去皮,切丁等)这样在不同的解释器里可以针对相同的方法给出不同的动作。 如对于电脑,则直接显示在屏幕上,对于语音控制设备,则发出语音指示,对于(以后可能出现的)做饭机器人,则执行动作等等。 我的想法是基于现有的语言开发多套解析语法相同,但是解析结果不同的库来适配这些情况,这样,每个文件描述的是做饭的流程,也可以使用原生的import之类的功能避免重复coding。 这是我的想法最后的文件描述(我也不知道描述了什么,但是思想应该传递到了):

import { Tomato, Water } from 'Cooklib/Ingredients'
import { Knife, Pot } from 'Cooklib/Tools'
async function MakeDish(peopleNum) {
try {
await Water.require('size', peopleNum * 1500, peopleNum * 2000) //1500至2000ml水
await Tomato.require('size', 100, 500) //要求每个在100克到500克之间,此时做饭人或机器可以自检,通过后继续执行
await Tomato.require('num', peopleNum * 3, peopleNum * 5) //要求3到5个
const TomatoPiece = await Knife.cutInPieces(Tomato)
await Pot.Put(Water, Water.size) //将 水 放入 锅 (其实倒入会更好)
await Pot.BoilUntilTemperature(Water.boilingPoint) //将水烧到沸点(沸点可以根据当地气压测量)
await Pot.put(TomatoPiece, 1.0) //放入100%数量的番茄碎
await Pot.BoilUntilTime(60 * 1000) //等待60000毫秒(一分钟)
return { status: 0, element: Pot.getElement() } //将锅中物品装盘备用(doge)
} catch (error) {
return { status: -1, err: err.code }
}
}

@tongque0
Copy link

tongque0 commented Apr 8, 2025

这里是新的设想,我们现在可以利用LLM的结构化输出的能力,帮助我们快速的获得指定格式的菜谱。同时借助openai的深度研究功能,提出了以下格式

🍽 通用菜谱数据格式(JSON Schema 示例)

{
  "id": "recipe-123456",
  "title": "西红柿炒鸡蛋",
  "author": "张三",
  "source": "https://example.com/tomato-egg",
  "description": "一道经典的家常菜,新手入门首选。",
  "recipeCuisine": "中餐",
  "recipeCategory": "主菜",
  "tags": ["家常菜", "快手菜", "蛋类"],
  "yield": "2 人份",
  "prepTime": "PT5M",
  "cookTime": "PT10M",
  "totalTime": "PT15M",
  "nutrition": {
    "calories": "150 kcal",
    "fatContent": "10 g",
    "proteinContent": "6 g"
  },
  "ingredients": [
    {
      "id": "tomato",
      "name": "番茄",
      "quantity": 1,
      "unit": "颗",
      "optional": false,
      "preparation": "切块"
    },
    {
      "id": "egg",
      "name": "鸡蛋",
      "quantity": 2,
      "unit": "个",
      "optional": false
    },
    {
      "id": "salt",
      "name": "盐",
      "quantity": 2,
      "unit": "g",
      "optional": false
    },
    {
      "id": "sugar",
      "name": "糖",
      "quantity": 1,
      "unit": "g",
      "optional": true
    },
    {
      "id": "oil",
      "name": "食用油",
      "quantity": 10,
      "unit": "mL"
    },
    {
      "id": "scallion",
      "name": "葱花",
      "quantity": 5,
      "unit": "g",
      "optional": true
    }
  ],
  "tools": ["炒锅", "锅铲", "碗"],
  "sections": [
    {
      "name": "准备食材",
      "steps": [
        {
          "id": "step-1",
          "text": "番茄洗净后切块备用。",
          "ingredients": ["tomato"]
        },
        {
          "id": "step-2",
          "text": "鸡蛋打入碗中,加入一半盐搅匀成蛋液。",
          "ingredients": ["egg", "salt"]
        }
      ]
    },
    {
      "name": "炒蛋",
      "steps": [
        {
          "id": "step-3",
          "text": "热锅,倒入食用油,油热后倒入蛋液。",
          "ingredients": ["oil"],
          "tools": ["炒锅"],
          "time": "PT1M"
        },
        {
          "id": "step-4",
          "text": "炒至鸡蛋成型略微金黄,盛出备用。",
          "depends_on": ["step-3"],
          "time": "PT1M"
        }
      ]
    },
    {
      "name": "炒番茄",
      "steps": [
        {
          "id": "step-5",
          "text": "锅中再加少量油,放入番茄翻炒至软烂。",
          "ingredients": ["tomato"],
          "time": "PT2M"
        },
        {
          "id": "step-6",
          "text": "加入炒好的鸡蛋翻炒均匀,加入剩余的盐和糖调味。",
          "ingredients": ["egg", "salt", "sugar"],
          "depends_on": ["step-5"]
        },
        {
          "id": "step-7",
          "text": "最后撒上葱花即可出锅。",
          "ingredients": ["scallion"],
          "optional": true
        }
      ]
    }
  ],
  "version": "1.0",
  "locale": "zh-CN",
  "datePublished": "2024-01-15"
}

✅ 特点说明:

字段名 说明
id 每个菜谱的唯一ID
title 菜谱名称
author、source 作者与原始链接
description 简要描述
recipeCuisine 菜系,例如中餐、意大利等
recipeCategory 类别,如主菜、甜品等
tags 标签,可用于分类或搜索
yield 产量说明(几人份)
prepTime、cookTime、totalTime ISO8601格式的时间字符串
nutrition 可选,包含热量、脂肪、蛋白质等营养信息
ingredients 食材数组,支持结构化字段如name、quantity、unit、optional
tools 用到的工具列表
sections 步骤分段(便于模块化),每段包含多个步骤
steps 每个步骤可包含id、text、ingredients、tools、time、depends_on
locale 菜谱使用的语言区域
version 格式版本号
datePublished 发布时间

如果此格式合适的话,我将编写对应golang程序,利用大模型的能力,生成一个以此格式为基础的菜谱json库。

@zgldh
Copy link
Author

zgldh commented Apr 8, 2025

我觉得重点是该规范的“表达能力”,和“扩展能力”。表达能力决定了该规范能覆盖多广的厨艺和食谱做法,扩展能力决定了未来有新的食材、厨具时能及时跟上不掉队。
这就对食材、厨具、厨艺的描述要细致且有条理。比如说做沙拉,用樱桃西红柿和普通西红柿吃起来就是差别很大。但做西红柿炒蛋吃起来味道却差别不大。烙饼用平底锅,但技术好的人用炒锅也能烙出不错的饼。
这套规范在描述上面这些情况时,要确保广度和深度都覆盖到才行。

@lumynou5
Copy link

lumynou5 commented Apr 8, 2025

我觉得重点是该规范的“表达能力”,和“扩展能力”。表达能力决定了该规范能覆盖多广的厨艺和食谱做法,扩展能力决定了未来有新的食材、厨具时能及时跟上不掉队。 这就对食材、厨具、厨艺的描述要细致且有条理。比如说做沙拉,用樱桃西红柿和普通西红柿吃起来就是差别很大。但做西红柿炒蛋吃起来味道却差别不大。烙饼用平底锅,但技术好的人用炒锅也能烙出不错的饼。 这套规范在描述上面这些情况时,要确保广度和深度都覆盖到才行。

假設有這麼一個機器能執行食譜,「廚具」、「處理方式」是取決於機器功能的,其實比較好處理,無非是加個新名稱。

比較麻煩的是食材,很難找到一個不過於繁瑣、又足夠嚴謹的表達方式,例如炒雞蛋通常我們不會去在意這是哪種雞產的,就算在意也只是個人偏好而不是食譜該涵蓋的,只要別用成皮蛋等顯然不對的都沒什麼問題。這個例子也許可以簡單用「生雞蛋」(註)來描述,但我們很難列出每種食材名稱並加以定義,例如番茄炒蛋不用聖女番茄(櫻桃番茄),但這不是唯一的小果番茄品種,最後變成我們得定義「大番茄」和「小番茄」是哪些品種的集合。考慮到就算真有這麼一個機器食材應該也是人類提供,以及目前食譜主要還是給人看的,可能還是只能用自然語言描述,人可以理解「大番茄」是什麼,哪怕不知道它們的學名。不過步驟是可以更嚴謹的,改進我之前那個 YAML 的設想:

# ...
  - dependencies:
      - 雞蛋
      - 
      - 
    tools:
      - 攪拌棒
    container: 
    steps:
      - do: 打蛋
        object: 雞蛋
      - do: 放入
        object: 
        amount: calc(1g * $份數)
      - do: 放入
        object: 
      - do: 攪勻
        with: 攪拌棒
    artifact: 雞蛋液
  - dependencies:
      - 食用油
      - 雞蛋液
      - 番茄塊
      - 
      - 
      - 蔥花
    tools:
      - 鍋鏟
      - 
    container: 
    steps:
      - do: 熱鍋
      - do: 放入
        object: 食用油
      - do: 等待
        until: 油熱
      - do: 放入
        object: 雞蛋液
      - do: 翻炒
        with: 鍋鏟
        until: 雞蛋結為固體且顏色微黃
      - do: 關火
      - do: 盛起
        with: 
      - do: 開火
      - do: 放入
        object: 番茄塊
      - do:
          - 拍打
          - 翻炒
        with: 鍋鏟
        for: 20s
        until: 番茄變得軟爛
      - do: 放入
        object: *盤
      - do: 翻炒
        with: 鍋鏟
      - do: 放入
        object: 
      - do: 放入
        object: 
      - do: 放入
        object: 蔥花
      - do: 翻炒
        with: 鍋鏟

不過我沒想到動作「結束條件」怎麼嚴謹描述,畢竟這些都是透過觀察(半)成品的狀態來達到的。另外,雖然將動作的謂語和賓語抽離出來更加形式化,但反過來說如果由 YAML 產生自然語言食譜會變得有點怪,例如「放入盤中物」而非更自然的「放入剛剛盛起的雞蛋」。

註:我當時沒想到,皮蛋的製作過程中沒有煮熟這一步驟,所以它可能其實算是生蛋。

@sheepzh
Copy link

sheepzh commented Apr 21, 2025

我发现使用谓宾+介补的语法设计形式语言,可以非常精简,也有比较高的准确度。然后我用 deepseek-r1 测试了可读性,发现它能够完全理解

比如一碗香的食谱

id: r#yi_wan_xiang
kitchenware: k#pot
preprocess:
    - slice: m#pork_lean 125g
    - slice: m#pork_fat 125g
    - ring_cut:
          - m#green_pepper 3
          - m#birds_eye_chili 1
    - mince: m#garlic 2
    - julienne: m#ginger 2
    - mix: m#chicken_egg 2
      till: smooth
steps:
    - fire: low
    - add: m#cooking_oil
    - heat:
      till: 70%
    - add: m#chicken_egg@mixed
    - fry:
      till: m#chicken_egg=cooked
    - take: m#chicken_egg
    - add: m#cooking_oil
    - heat:
      till: 70%
    - add: m#pork_fat
    - dry_fry:
      till: m#pork_fat=color_golden
    - fire: medium
    - add: m#pork_lean
    - fry:
      till: m#pork_lean=color_changed
    - add:
          - m#ginger@julienned
          - m#garlic@minced
          - m#pixian_doubanjiang
    - fry:
      till: m#pork_lean=color_red
    - add:
          - m#green_pepper
          - m#birds_eye_chili
          - m#chicken_egg=cooked
          - m#soy_sauce 15ml
          - m#granulated_sugar 5g
    - fry:
      till: m#green_pepper=cooked

以下是 ds 的结果,可读性上来说,效果还不错。(prompt=假如你是一位爱好烹饪的新手,你能够理解我的这个 YAML 文件吗? \n{yaml_content}

Image

这是菜谱原文
Image

当然,还有 ds 的建议

Image

其实我觉得用 ds 来做白盒评测感觉也不错

@sheepzh
Copy link

sheepzh commented Apr 21, 2025

我发现使用谓宾+介补的语法设计形式语言,可以非常精简,也有比较高的准确度。然后我用 deepseek-r1 测试了可读性,发现它能够完全理解

然后菜谱文件里的原料,可以定义另外一套 json-schema,声明对应的行为。比如说规定蒜(m#garlic)只能 slice,mince,对应的产物就是蒜片(m#garlic@sliced) 和蒜末(m#garlic@minced)。然后大蒜或者独头蒜可以同时继承 (extend) 蒜这个原料,除了 ID,不需要修改其他任何字段,他们各自的特点可以用一个扩展字段描述,比如 spec。写菜谱的时候,根据需要选择具体的原料即可

比如

id: m#garlic 
category: seasoning
unitType: count
preprocess: [slice, mince]

独头蒜

id: m#solo_garlic
extend: m#garlic

@freysu
Copy link

freysu commented Apr 22, 2025

可以先编写几个基本类工具,食材等等,然后派生成为更详细的内容。 如番茄,,这些,然后抽象出方法(对于锅来说,有,对于番茄来说有去皮,切丁等)这样在不同的解释器里可以针对相同的方法给出不同的动作。 如对于电脑,则直接显示在屏幕上,对于语音控制设备,则发出语音指示,对于(以后可能出现的)做饭机器人,则执行动作等等。 我的想法是基于现有的语言开发多套解析语法相同,但是解析结果不同的库来适配这些情况,这样,每个文件描述的是做饭的流程,也可以使用原生的import之类的功能避免重复coding。 这是我的想法最后的文件描述(我也不知道描述了什么,但是思想应该传递到了):

感谢思路分享~我昨晚用claude3.7sonnet来简单实现了一个菜谱创建工具,欢迎大家来玩一下。
github page部署地址: https://freysu.github.io/recipe-creator-simple/
仓库地址: https://github.com/freysu/recipe-creator-simple,

操作截图-测试用例-番茄炒蛋.png

预定义了食材库(肉类,海鲜,蔬菜,调料),工具库(锅具,刀具,容器,厨具,电器,案板,计量工具,其他), 操作类型(预定义了 20+ 种标准化烹饪操作(如 wash、cut、heat、stir 等),每种操作都有明确的参数定义(如火力大小、切割方式、持续时间))

相关的源代码: https://github.com/freysu/recipe-creator-simple/blob/28558d704f8ff9664db06904fe048d4b5854ff22/js/app.js#L44

我感觉ai设计的好像还可以。就是好像实际添加有点麻烦。

程序导出的 MARKDOWN 格式:

# 西红柿炒鸡蛋的做法

西红柿炒蛋是中国家常几乎最常见的一道菜肴。它的原材料易于搜集,制作步骤也较为简单,所以非常适合新厨师上手,是很多人学习做菜时做的第一道菜。

预估烹饪难度:★★★

## 必备原料和工具

- 番茄 1个
- 鸡蛋 2个
- 食用油 8毫升
- 盐 2克
- 糖 2克
- 葱 10克

- 炒锅
- 铲子
-- 菜板
- 菜刀

## 计算

每次制作前需要确定计划做几份。一份正好够 1 个人吃。

每份:

- 番茄 1个
- 鸡蛋 2个
- 食用油 8毫升
- 盐 2克
- 糖 2克
- 葱 10克

## 操作

- 西红柿洗净
  - 清洗番茄
  - **等待 30秒**
  - 直到*西红柿洗净*

- 去掉西红柿的外表皮(可选)
  - 剥皮/去皮番茄
  - **等待 1分钟**
  - 直到*西红柿去皮完成*

- 西红柿去蒂,切成小块
  - 将番茄切块(4厘米)
  - **等待 1分钟**
  - 直到*西红柿切成边长不超过4cm的小块*

- 将鸡蛋打入碗中,加盐搅匀
  - 加入鸡蛋
  - 加入盐
  - 混合碗
  - **等待 1分钟**
  - 直到*鸡蛋液搅拌均匀*

- 热锅加油,倒入鸡蛋液炒至半熟
  - 中火加热炒锅
  - 加入食用油
  - 加入鸡蛋
  - 翻炒/搅拌(30秒)
  - **等待 1分钟**
  - 直到*鸡蛋结为固体且颜色微微发黄*

- 关火,将半熟鸡蛋盛出,重新开火
  - 装盘/上菜
  - **等待 10秒**
  - 直到*鸡蛋盛出备用*

- 加入西红柿块,拍打并翻炒
  - 加入番茄
  - 翻炒/搅拌(20秒)
  - **等待 30秒**
  - 直到*西红柿软烂*

- 向锅中加入半熟鸡蛋,翻炒均匀
  - 加入鸡蛋
  - 翻炒/搅拌(30秒)
  - **等待 30秒**
  - 直到*鸡蛋与西红柿混合均匀*

- 加入调味料
  - 加入盐
  - 加入糖
  - 加入葱
  - 翻炒/搅拌(10秒)
  - **等待 20秒**
  - 直到*调味均匀*

- 关火,盛盘
  - 装盘/上菜
  - **等待 10秒**
  - 直到*菜品装盘完成*

## 附加内容

- 如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。

相关标签: 素菜, 家常菜

程序导出的json格式:

{
  "recipe": {
    "name": "西红柿炒鸡蛋",
    "description": "西红柿炒蛋是中国家常几乎最常见的一道菜肴。它的原材料易于搜集,制作步骤也较为简单,所以非常适合新厨师上手,是很多人学习做菜时做的第一道菜。",
    "servings": 1,
    "difficulty": "medium",
    "tags": ["素菜", "家常菜"],
    "ingredients": [
      {
        "id": "tomato",
        "name": "番茄",
        "amount": 1,
        "unit": "",
        "category": "vegetable",
        "properties": {
          "acidity": "high"
        }
      },
      {
        "id": "egg",
        "name": "鸡蛋",
        "amount": 2,
        "unit": "",
        "category": "other",
        "properties": {
          "protein": "high"
        }
      },
      {
        "id": "oil",
        "name": "食用油",
        "amount": 8,
        "unit": "毫升",
        "category": "other",
        "properties": {
          "fat": "high"
        }
      },
      {
        "id": "salt",
        "name": "",
        "amount": 2,
        "unit": "",
        "category": "seasoning",
        "properties": {
          "saltiness": "high"
        }
      },
      {
        "id": "sugar",
        "name": "",
        "amount": 2,
        "unit": "",
        "category": "seasoning",
        "properties": {
          "sweetness": "high"
        }
      },
      {
        "id": "scallion",
        "name": "",
        "amount": 10,
        "unit": "",
        "category": "vegetable",
        "properties": {
          "pungency": "medium"
        }
      }
    ],
    "tools": [
      {
        "id": "wok",
        "name": "炒锅",
        "type": "pan",
        "properties": {
          "material": "iron",
          "size": "large"
        }
      },
      {
        "id": "spatula",
        "name": "铲子",
        "type": "utensil",
        "properties": {
          "material": "silicone"
        }
      },
      {
        "id": "bowl",
        "name": "",
        "type": "container",
        "properties": {
          "size": "medium"
        }
      },
      {
        "id": "cuttingBoard",
        "name": "菜板",
        "type": "board",
        "properties": {
          "material": "wood"
        }
      },
      {
        "id": "chefKnife",
        "name": "菜刀",
        "type": "knife",
        "properties": {
          "sharpness": "high"
        }
      }
    ],
    "steps": [
      {
        "description": "西红柿洗净",
        "actions": [
          {
            "type": "wash",
            "target": {
              "id": "tomato",
              "name": "番茄",
              "type": "ingredient"
            },
            "params": {}
          }
        ],
        "duration": "30秒",
        "condition": "西红柿洗净"
      },
      {
        "description": "去掉西红柿的外表皮(可选)",
        "actions": [
          {
            "type": "peel",
            "target": {
              "id": "tomato",
              "name": "番茄",
              "type": "ingredient"
            },
            "params": {}
          }
        ],
        "duration": "1分钟",
        "condition": "西红柿去皮完成"
      },
      {
        "description": "西红柿去蒂,切成小块",
        "actions": [
          {
            "type": "cut",
            "target": {
              "id": "tomato",
              "name": "番茄",
              "type": "ingredient"
            },
            "params": {
              "style": "切块",
              "size": "4厘米"
            }
          }
        ],
        "duration": "1分钟",
        "condition": "西红柿切成边长不超过4cm的小块"
      },
      {
        "description": "将鸡蛋打入碗中,加盐搅匀",
        "actions": [
          {
            "type": "add",
            "target": {
              "id": "egg",
              "name": "鸡蛋",
              "type": "ingredient"
            },
            "params": {}
          },
          {
            "type": "add",
            "target": {
              "id": "salt",
              "name": "",
              "type": "ingredient"
            },
            "params": {}
          },
          {
            "type": "mix",
            "target": {
              "id": "bowl",
              "name": "",
              "type": "tool"
            },
            "params": {}
          }
        ],
        "duration": "1分钟",
        "condition": "鸡蛋液搅拌均匀"
      },
      {
        "description": "热锅加油,倒入鸡蛋液炒至半熟",
        "actions": [
          {
            "type": "heat",
            "target": {
              "id": "wok",
              "name": "炒锅",
              "type": "tool"
            },
            "params": {
              "level": "中火"
            }
          },
          {
            "type": "add",
            "target": {
              "id": "oil",
              "name": "食用油",
              "type": "ingredient"
            },
            "params": {}
          },
          {
            "type": "add",
            "target": {
              "id": "egg",
              "name": "鸡蛋",
              "type": "ingredient"
            },
            "params": {}
          },
          {
            "type": "stir",
            "target": {
              "id": "spatula",
              "name": "铲子",
              "type": "tool"
            },
            "params": {
              "duration": "30秒"
            }
          }
        ],
        "duration": "1分钟",
        "condition": "鸡蛋结为固体且颜色微微发黄"
      },
      {
        "description": "关火,将半熟鸡蛋盛出,重新开火",
        "actions": [
          {
            "type": "serve",
            "target": {
              "id": "bowl",
              "name": "",
              "type": "tool"
            },
            "params": {}
          }
        ],
        "duration": "10秒",
        "condition": "鸡蛋盛出备用"
      },
      {
        "description": "加入西红柿块,拍打并翻炒",
        "actions": [
          {
            "type": "add",
            "target": {
              "id": "tomato",
              "name": "番茄",
              "type": "ingredient"
            },
            "params": {}
          },
          {
            "type": "stir",
            "target": {
              "id": "spatula",
              "name": "铲子",
              "type": "tool"
            },
            "params": {
              "duration": "20秒"
            }
          }
        ],
        "duration": "30秒",
        "condition": "西红柿软烂"
      },
      {
        "description": "向锅中加入半熟鸡蛋,翻炒均匀",
        "actions": [
          {
            "type": "add",
            "target": {
              "id": "egg",
              "name": "鸡蛋",
              "type": "ingredient"
            },
            "params": {}
          },
          {
            "type": "stir",
            "target": {
              "id": "spatula",
              "name": "铲子",
              "type": "tool"
            },
            "params": {
              "duration": "30秒"
            }
          }
        ],
        "duration": "30秒",
        "condition": "鸡蛋与西红柿混合均匀"
      },
      {
        "description": "加入调味料",
        "actions": [
          {
            "type": "add",
            "target": {
              "id": "salt",
              "name": "",
              "type": "ingredient"
            },
            "params": {}
          },
          {
            "type": "add",
            "target": {
              "id": "sugar",
              "name": "",
              "type": "ingredient"
            },
            "params": {}
          },
          {
            "type": "add",
            "target": {
              "id": "scallion",
              "name": "",
              "type": "ingredient"
            },
            "params": {}
          },
          {
            "type": "stir",
            "target": {
              "id": "spatula",
              "name": "铲子",
              "type": "tool"
            },
            "params": {
              "duration": "10秒"
            }
          }
        ],
        "duration": "20秒",
        "condition": "调味均匀"
      },
      {
        "description": "关火,盛盘",
        "actions": [
          {
            "type": "serve",
            "target": {
              "id": "tomato",
              "name": "番茄",
              "type": "ingredient"
            },
            "params": {}
          }
        ],
        "duration": "10秒",
        "condition": "菜品装盘完成"
      }
    ]
  },
  "metadata": {
    "version": "1.0",
    "format": "robot-executable",
    "timestamp": "2025-04-22T02:53:01.984Z"
  }
}

@bainianlaoyao
Copy link

bainianlaoyao commented Apr 26, 2025

使用配置式的描述会有大量冗余, 我在这里提议使用面向对象和函数式的结合, 来最大程度的精简菜谱的定义.

在这种方法下, 基础工作会需要一些劳力, 为了不吓到各位, 首先给出一个简单炒土豆的菜谱定义的最终结果

本方法下, 所有代码都可以实际运行, 并已经基本实现生成markdown功能. 我会在文末放出完整代码, 原生实现, 无外部依赖

class 炒土豆(Meal):
    def __init__(self,weight=100):
        super().__init__('炒土豆', weight)
        self.potato = 土豆(weight)
        self.require = [self.potato]
        # self.extend_submeal([self.potato])
        self.steps = [
            切块(self.potato),
            ((3),10,5),
            炒_调料(self.potato,30,20,[(5),(3)],[10,20]) # 先炒蒜,再加入土豆和盐,
        ]
        self.auto_req()

并能根据此生成结果:


菜名: 炒土豆

工具要求

调料要求

  • 油 (5g)
  • 油 (20g)
  • 盐 (5g)
  • 盐 (3g)

食材要求 (每份)

  • 土豆 (500g)

最终营养成分

  • 淀粉: 100.0
  • 水分: 400.0
  • 脂肪: 25
  • 钠: 8

合并重量

  • 土豆: 500g
  • 油: 25g
  • 盐: 8g

步骤

  • 拿出食材 土豆
    将土豆切成块
  • 将5克油放入锅中, 炒 蒜 10 秒
  • 将20克油放入锅中, 炒 切块土豆 30 秒
    在10秒时加入盐 5g
    在20秒时加入盐 3g

可以看到, 我们定义了一个土豆作为原材料,. 并使用了两个步骤, 切快和炒.

实际上我做的, 就是借助python当我的AST分析器, 如果我们试图标准化的定义菜谱, 那么就一定能够将菜谱解析为AST. 因此, 借助编程语言解析AST并无不妥.
同时, 这种方法还实现了极好的封装性, 请看我自创的暗黑炖土豆:

class 黑暗炖土豆(Meal):
    def __init__(self, weight=0):
        super().__init__("黑暗炖土豆", weight)
        self.is_show_submeal = True
        self.potato = 炒土豆(weight)
        self.extend_submeal([self.potato])
        self.submeal.append(self.potato)
        self.steps = [
            # 切块([self.potato]),
            (self.potato,120)
        ]
        self.auto_req()

可以看到, 我使用了炒土豆作为一个前置菜品, 这非常符合菜谱中需要的各种复杂准备, 并且无须重复定义! 而且参数可调.
生成结果:


菜名: 黑暗炖土豆

工具要求

调料要求

  • 油 (5g)
  • 油 (20g)
  • 盐 (5g)
  • 盐 (3g)

食材要求 (每份)

  • 土豆 (500g)

最终营养成分

  • 淀粉: 100.0
  • 水分: 400.0
  • 脂肪: 25
  • 钠: 8

合并重量

  • 土豆: 500g
  • 油: 25g
  • 盐: 8g

步骤

  • 拿出食材 土豆
    将土豆切成块
  • 将5克油放入锅中, 炒 蒜 10 秒
  • 将20克油放入锅中, 炒 切块土豆 30 秒
    在10秒时加入盐 5g
    在20秒时加入盐 3g
  • 将炒土豆放入煮开水的锅中,炖 120 秒

完整代码:

from copy import deepcopy
from functools import reduce

class Food:
    def __init__(self, name='', weight=0):
        self.require : list[Food]= [self]
        self.tool_require : list[Tool] = []
        self.spice_require : list[Food]= []
        self.name = name
        self.weight = weight
        self.method = None
        self.营养成分 = {}
    def clone(self):
        return deepcopy(self)
    def info(self):
        return f"{self.name} ({self.weight}g)"
    def generate(self) -> str:
        return f"拿出食材 {self.name}"
class (Food):
    def __init__(self, weight=0):
        super().__init__('油', weight)
        self.营养成分={'脂肪' : weight}
class (Food):
    def __init__(self, weight=0):
        super().__init__('盐', weight)
        self.营养成分 = {'钠': weight}
class (Food):
    def __init__(self, weight=0):
        super().__init__('蒜', weight)

class Tool:
    def __init__(self):
        self.name = ''
        pass
class (Tool):
    def __init__(self,name = '刀'):
        super().__init__()
        self.name = name      
        pass
class (Tool):
    def __init__(self,name = '锅'):
        super().__init__()
        self.name = name
        pass
class Method:
    def __init__(self):
        self.require = []
        self.spice_require = []
        self.tool_require = []
        pass
    def process(self) -> Food:
        return '无事发生'
class 切块(Method):
    def __init__(self,food : Food):
        super().__init__()
        self.tool_require = [()]
        
        self.food = food
        pass
    def process(self) -> Food:
        description= self.food.generate()
        description += f"\n{self.food.name}切成块"
        self.food.name = f"切块{self.food.name}"
        self.food.method = self
        return description
class (Method):
    def __init__(self, food : Food,time = 30,oil=2):
        super().__init__()
        self.spice_require = [(oil)]
        self.tool_require = [()]
        self.time = time
        
        self.food = food
    def process(self) -> Food:
        description = f"将{self.spice_require[0].weight}克油放入锅中, 炒 {self.food.name} {self.time} 秒"
        self.food.name = f"炒{self.food.name}"
        self.food.method = self
        return description
class 炒_调料():
    def __init__(self, food : Food,time = 30,oil=1,spice_require = [], add_time = []):
        super().__init__(food,time,oil)
        if len(spice_require) != len(add_time):
            assert "调料和时间数量不匹配"
        self.spice_require.extend(spice_require)
        self.add_time = add_time
    def process(self) -> Food:
        description = f"将{self.spice_require[0].weight}克油放入锅中, 炒 {self.food.name} {self.time} 秒"
        for i in range(len(self.add_time)):
            description = description + f'\n{self.add_time[i]}秒时加入{self.spice_require[i+1].name} {self.spice_require[i+1].weight}g'
        self.food.name = f"炒{self.food.name}"
        self.food.method = self
        return description
         
class 加调料(Method):
    def __init__(self, spice : Food):
        super().__init__()
        self.spice_require = [spice]
        self.spice = spice
    def process(self) -> Food:
        description = f"加入{self.spice.name}"
        return description
class (Method):
    def __init__(self, food: Food, time=30, end_result = ''):
        super().__init__()
        self.tool_require = [()]
        self.time = time
        self.end_result = end_result
        self.food = food
    def process(self):
        description = f"将{self.food.name}放入煮开水的锅中,炖 {self.time} 秒 "
        self.food.name = f"炖{self.food.name if not self.end_result else self.end_result}"
        return description

    
class 土豆(Food):
    def __init__(self, weight=0):
        super().__init__('土豆', weight)
        self.营养成分 = {
            '淀粉': 0.2 * weight,
            '水分': 0.8 * weight
        }
class Meal(Food):
    def __init__(self, name='', weight=0):
        super().__init__(name, weight)
        self.require : list[Food] = []
        self.steps : list[Method] = []
        self.submeal : list[Meal] = []
        self.is_show_submeal = True
    def generate(self,level=0) -> str:
        md = []
        # 标题
        md.append(f"#{'#'*level} 菜名: {self.name}")
        md.append(f"##{'#'*level} 工具要求")
        # 列出唯一工具
        unique_tools = []
        for tool in self.tool_require:
            if tool.name not in unique_tools:
                unique_tools.append(tool.name)
        md.append("\n".join([f"- {name}" for name in unique_tools]))
        # 调料要求
        md.append(f"##{'#'*level} 调料要求")
        md.append("\n".join([f"- {food.name} ({food.weight}g)" for food in self.spice_require]))
        # 食材要求 (每份)
        md.append(f"##{'#'*level} 食材要求 (每份)")
        md.append("\n".join([f"- {food.name} ({food.weight}g)" for food in self.require]))
        # 计算总营养成分
        total_nutrition = {}
        for item in self.require + self.spice_require:
            for key, val in item.营养成分.items():
                total_nutrition[key] = total_nutrition.get(key, 0) + val
        md.append(f"##{'#'*level} 最终营养成分")
        md.append("\n".join([f"- {k}: {v}" for k, v in total_nutrition.items()]))
        # 合并重量
        weight_summary = {}
        for item in self.require + self.spice_require:
            weight_summary[item.name] = weight_summary.get(item.name, 0) + item.weight
        md.append(f"##{'#'*level} 合并重量")
        md.append("\n".join([f"- {name}: {weight}g" for name, weight in weight_summary.items()]))
        # 步骤
        md.append(f"##{'#'*level} 步骤")
        steps_md = []
        
        if self.is_show_submeal:
            for meal in self.submeal:
                for step in meal.steps:
                    steps_md.append(f"- {step.process()}")

        # 各个处理步骤
        for step in self.steps:
            steps_md.append(f"- {step.process()}")
        md.append("\n".join(steps_md))
        return "\n\n".join(md)
    def auto_req(self):
        for step in self.steps:
            self.tool_require.extend(step.tool_require)
            self.spice_require.extend(step.spice_require)
    def extend_submeal(self, food_lsit: list[Food]):
        for food in food_lsit:
            self.require.extend(food.require)
            self.tool_require.extend(food.tool_require)
            self.spice_require.extend(food.spice_require)
            self.营养成分 = reduce(lambda x, y: {k: x.get(k, 0) + y.get(k, 0) for k in set(x) | set(y)}, [self.营养成分] + [food.营养成分 for food in food_lsit])

        
        
class 炒土豆(Meal):
    def __init__(self,weight=100):
        super().__init__('炒土豆', weight)
        self.potato = 土豆(weight)
        self.require = [self.potato]
        # self.extend_submeal([self.potato])
        self.steps = [
            切块(self.potato),
            ((3),10,5),
            炒_调料(self.potato,30,20,[(5),(3)],[10,20]) # 先炒蒜,再加入土豆和盐,
        ]
        self.auto_req()

class 黑暗炖土豆(Meal):
    def __init__(self, weight=0):
        super().__init__("黑暗炖土豆", weight)
        self.is_show_submeal = True
        self.potato = 炒土豆(weight)
        self.extend_submeal([self.potato])
        self.submeal.append(self.potato)
        self.steps = [
            # 切块([self.potato]),
            (self.potato,120)
        ]
        self.auto_req()
if __name__ == '__main__':
    # meal = 炒土豆( 500)
    meal = 黑暗炖土豆( 500)
    print(meal.generate())
    # print(meal.potato.export())
    # print(meal.potato.营养成分)  # 输出营养成分

@lumynou5
Copy link

lumynou5 commented Apr 27, 2025

那 OOP 感覺不是最適合的(誤

(炒-分段
  '('(10 's)
    (油 '(5 'g))
    (蒜 '(3 'g)))
  '('(10 's)
    (油 '(20 'g))
    (切塊 (馬鈴薯 '(100 'g))))
  '('(10 's)
    (鹽 '(5 'g)))
  '('(10 's)
    (鹽 '(3 'g))))

對比

class 炒土豆(Meal):
    def __init__(self,weight=100):
        super().__init__('炒土豆', weight)
        self.potato = 土豆(weight)
        self.require = [self.potato]
        # self.extend_submeal([self.potato])
        self.steps = [
            切块(self.potato),
            ((3),10,5),
            炒_调料(self.potato,30,20,[(5),(3)],[10,20]) # 先炒蒜,再加入土豆和盐,
        ]
        self.auto_req()

另外,你忘了憑空冒出來的蒜。

不過個人認為 config language 再怎麼繁瑣都不會比 programming language 來得繁瑣,而且可擴充性更佳。例如你需要學會一個程式語言,你得加上一些「不相關的雜事」,例如呼叫 self.auto_seq() 等等,而 YAML 等可以專注於撰寫食譜本身。

@bainianlaoyao
Copy link

bainianlaoyao commented Apr 28, 2025

那 OOP 感覺不是最適合的((

(炒-分段
'('(10 's)
(油 '(5 'g))
(蒜 '(3 'g)))
'('(10 's)
(油 '(20 'g))
(切塊 (馬鈴薯 '(100 'g))))
'('(10 's)
(鹽 '(5 'g)))
'('(10 's)
(鹽 '(3 'g))))
對比

class 炒土豆(Meal):
def init(self,weight=100):
super().init('炒土豆', weight)
self.potato = 土豆(weight)
self.require = [self.potato]
# self.extend_submeal([self.potato])
self.steps = [
切块(self.potato),
炒(蒜(3),10,5),
炒_调料(self.potato,30,20,[盐(5),盐(3)],[10,20]) # 先炒蒜,再加入土豆和盐,
]
self.auto_req()
另外,你忘了憑空冒出來的蒜。

不過個人認為 config language 再怎麼繁瑣都不會比 programming language 來得繁瑣,而且可擴充性更佳。例如你需要學會一個程式語言,你得加上一些「不相關的雜事」,例如呼叫 self.auto_seq() 等等,而 YAML 等可以專注於撰寫食譜本身。

你看漏了, 我写了蒜的实现. 如果你想要解析菜谱, 而不是让人直接瞪着config, 那就终究要使用编程语言, 我不知道为什么你觉得"可扩充性更佳". 如果你试图自由的扩展config的语法, 解析上也要对应的更改, 除非你寄希望于llm.

另外你看得见`self.auto_seq()'只是我缺少时间做出完善的用户体验, 实际上我完全可以只要求用户写

 切块(self.potato),
((3),10,5),
炒_调料(self.potato,30,20,[(5),(3)],[10,20]) # 先炒蒜,再加入土豆和盐,

然后使用

exec(f"self.steps = [{str}]")

也可以说我使用python重新抽象出了一种配置语言, 只不过由于工作量原因我直接复用了python的lexer, 因此这种语言需要遵循python语法

另一个使用现有language的优势在于你自然的获得了这个language已有的优秀自动补全和语法检查生态.

@lumynou5
Copy link

我是指你沒把蒜算到食材裡。


如果你想要解析菜谱, 而不是让人直接瞪着config, 那就终究要使用编程语言

當然總是要寫出個解析器的,但這和讓撰寫食譜的人直接接觸程式語言是兩回事。

也可以说我使用python重新抽象出了一种配置语言, 只不过由于工作量原因我直接复用了python的lexer, 因此这种语言需要遵循python语法

另一个使用现有language的优势在于你自然的获得了这个language已有的优秀自动补全和语法检查生态.

認同後一點,但 YAML 等非程式語言也都有現成的 language server,可以透過定義 schema 來補全可以使用的鍵名。在這點上兩種方案同樣有優勢,不具此優勢而工作量加大的方案只有自行設計專門的 DSL。

而受限於 Python 的語法就是一種「可擴充性不佳」,即便不需要 self.auto_seq(),也總得定義 class、定義 __init__(假如使用 OOP 設計)。且程式語言除了對非程式設計師不友善外,還有彈性過大的問題,食譜不需要圖靈完備,不需要可以寫出 蒜(float('nan')),也不需要可能會有「惡意食譜」。

@bainianlaoyao
Copy link

bainianlaoyao commented May 2, 2025

我是指你沒把蒜算到食材裡。

这确实是个bug

而受限於 Python 的語法就是一種「可擴充性不佳」,即便不需要 self.auto_seq(),也總得定義 class、定義 __init__(假如使用 OOP 設計)。且程式語言除了對非程式設計師不友善外,還有彈性過大的問題,食譜不需要圖靈完備,不需要可以寫出 蒜(float('nan')),也不需要可能會有「惡意食譜」。

我没有要求用户使用python, 我真正提出的只是一种函数式的描述方法, 也就是

 切块(self.potato),
((3),10,5),
炒_调料(self.potato,30,20,[(5),(3)],[10,20]) # 先炒蒜,再加入土豆和盐,

其他代码部分只是为了证明我的这种方案切实可行. 你可以用任何范式, 任何语言实现解析后端.
另外, python的语法的可扩充性不佳? 我只会觉得python的语法过于自由.

食譜不需要圖靈完備

图灵完备的食谱这件事本来就很酷

蒜(float('nan'))

这种情况首先会报错, 其次用户想写什么事用户的自由, 你使用纯文本也不能阻止用户写出乱七八糟的内容

也不需要可能會有「惡意食譜」。

即使有点大材小用, 但是在一个沙盒中运行代码并取出生成的markdown会注入攻击的成本高到完全不相称, 就算有人真的这么厉害且闲, 一个回滚和排查就能解决所有事情.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
open discussion Discussion for long-term enhancement
Projects
None yet
Development

No branches or pull requests