第 3 课
Recipe:带参数的可复用任务与结构化 JSON 输出
写一份带必填、可选参数和 response.json_schema 的 Recipe,用 recipe validate、--explain、--render-recipe 校验和预览,运行拿到固定结构的 JSON 并通过改提示词修正结果,再实测 file 类型参数的多行缩进坑与 indent() 解法,最后用 GOOSE_RECIPE_PATH、deeplink 和 schedule 管理与定时运行。
课程目录第 3 / 3 课
学习位置仅保存在当前浏览器,有效期 180 天。
图文教材
本课依据:goose 官方文档「Recipe Reference Guide」https://goose-docs.ai/docs/guides/recipes/recipe-reference 、「Reusable Recipes」https://goose-docs.ai/docs/guides/recipes/session-recipes 、「goose Recipes 教程」https://goose-docs.ai/docs/tutorials/recipes-tutorial 、「CLI Commands」https://goose-docs.ai/docs/guides/goose-cli-commands (2026-10-28 核对,goose 1.56.0)。
这一课做完你会得到什么
把「统计一个文件有多少行并说明它做什么」写成一份 Recipe(可复用的任务配方):带必填和可选参数,运行前先校验、预览,运行后输出固定格式的 JSON,方便脚本直接读取。最后再试一次 file 类型参数,看清它的一个坑和官方给的解法。
接着前两课的环境:GOOSE_PROVIDER=local,模型是 unsloth/gemma-4-E2B-it-GGUF:Q4_K_M,写在 ~/.config/goose/config.yaml 里。
第 1 步:准备练习目录和被统计的文件
mkdir -p /tmp/gt3 && cd /tmp/gt3
printf 'def add(a, b):\n return a + b\n\n\n' > calc.py
printf 'def div(a, b):\n return a / b\n' >> calc.py
wc -l calc.py
对照结果:
6 calc.py
第 2 步:写第一份 Recipe
官方推荐用 .yaml 扩展名,也支持 .json。注意:goose 命令行不支持 .yml,扩展名写成 .yml 时请改为 .yaml。
cat > count-lines.yaml <<'EOF'
version: "1.0.0"
title: "统计代码行数"
description: "统计指定文件的行数并给出一句话说明"
parameters:
- key: file
input_type: string
requirement: required
description: "要统计的文件路径"
- key: lang
input_type: string
requirement: optional
default: "中文"
description: "回答语言"
extensions:
- type: builtin
name: developer
prompt: |
用 shell 工具执行 wc -l {{ file }},
然后用{{ lang }}回答:文件 {{ file }} 有多少行,并用一句话说明它做什么。
response:
json_schema:
type: object
properties:
file:
type: string
lines:
type: number
summary:
type: string
required:
- file
- lines
- summary
EOF
逐段看它做了什么:
| 字段 | 作用 |
|---|---|
title、description | 必填,名称和说明 |
parameters | 参数列表,{{ file }} 这样的占位符会被替换 |
extensions | 运行时加载的扩展,这里带上 developer 才能执行命令 |
prompt | 发给 goose 的任务,无头运行时必填 |
response.json_schema | 要求最后输出符合这个结构的 JSON |
官方规定:prompt 和 instructions 至少写一个;可选参数必须给默认值,必填参数不能有默认值。
第 3 步:校验、查看参数、预览渲染结果
这三条命令都不调用模型,几秒内返回。
goose recipe validate count-lines.yaml
✓ recipe file is valid
故意写一份缺 description 的文件,看看报错是什么样:
printf 'version: "1.0.0"\ntitle: "bad"\nprompt: "hi"\n' > bad.yaml
goose recipe validate bad.yaml; echo exit=$?
Error: ✗ recipe file is invalid: missing field `description`
exit=1
退出码是 1,可以放进脚本或 CI 里先检查再运行。
查看这份 Recipe 需要哪些参数:
goose run --recipe count-lines.yaml --explain
🔍 Loading recipe: 统计代码行数
📄 Description:
统计指定文件的行数并给出一句话说明
⚙️ Recipe Parameters:
- file (string, required): 要统计的文件路径
- lang (string, optional) (default: 中文): 回答语言
📥 Parameters used to load this recipe:
lang: 中文
recipe_dir (built-in): /tmp/gt3
🔴 Missing parameters in the command line if you want to run the recipe:
- file
📩 Please provide the following parameters in the command line
if you want to run the recipe::
--params file=your_value
(倒数第二行在终端里是一整行,这里为排版折行。)
recipe_dir 是内置参数,值为 Recipe 所在目录,可以用 {{ recipe_dir }}/style-guide.md 这样的写法引用同目录的文件。
传入参数,只渲染、不运行:
goose run --recipe count-lines.yaml --params file=calc.py --render-recipe
对照结果(节选开头):
version: 1.0.0
title: 统计代码行数
description: 统计指定文件的行数并给出一句话说明
prompt: |
用 shell 工具执行 wc -l calc.py,
然后用中文回答:文件 calc.py 有多少行,并用一句话说明它做什么。
extensions:
- type: builtin
name: developer
占位符都替换好了,没有传的 lang 用了默认值「中文」。实测渲染结果里的 extensions 还多出一个 type: platform、name: analyze 的扩展,是 goose 1.56.0 自动加上的,不是我们写的。
第 4 步:运行并拿到 JSON
time goose run --recipe count-lines.yaml --params file=calc.py
实测输出(盒子 CPU 运行 2B 模型,用时约 2 分 32 秒):
▸ shell
command: wc -l calc.py
6 calc.py文件 `calc.py` 有 6 行。
由于我没有 `calc.py` 的源代码,我无法说明它具体是做什么的。
────────────────────────────────────────
▸ final_output recipe
file: calc.py
lines: 6
summary: 由于我没有 calc.py 的源代码,我无法说明它具体是做什么的。
Final output successfully collected.{"file":"calc.py","lines":6,
"summary":"由于我没有 calc.py 的源代码,我无法说明它具体是做什么的。"}
为了不超出页面宽度,最后一行 JSON 在这里折成了两行,终端里是一整行,紧跟在 Final output successfully collected. 后面。
两点值得注意:
- 写了
response.json_schema之后,goose 会调用名为final_output的工具交出结果,并按结构校验,所以file、lines、summary三个字段一个不少; - 行数对了,但
summary没用:任务只让它跑wc -l,模型并没有看到文件内容。这不是 goose 出错,是提示词没写清楚。
第 5 步:改提示词,再跑一次
把 prompt 改成先读文件、再数行。复制一份改好的文件:
NEW='先用 shell 工具执行 cat {{ file }} 读出内容,再执行 wc -l'
sed "s/用 shell 工具执行 wc -l/$NEW/" count-lines.yaml \
> count-lines-v2.yaml
grep -A2 '^prompt' count-lines-v2.yaml
prompt: |
先用 shell 工具执行 cat {{ file }} 读出内容,再执行 wc -l {{ file }},
然后用{{ lang }}回答:文件 {{ file }} 有多少行,并用一句话说明它做什么。
time goose run --recipe count-lines-v2.yaml --params file=calc.py
实测输出(节选,... 处省略了命令输出,用时约 3 分 47 秒):
▸ shell
command: cat calc.py
...
▸ shell
command: wc -l calc.py
...
▸ final_output recipe
file: calc.py
lines: 6
summary: 它定义了两个函数:`add` 用于计算两个数的和,
`div` 用于计算两个数的商。
Final output successfully collected.{"file":"calc.py","lines":6,
"summary":"它定义了两个函数:`add` 用于计算两个数的和,
`div` 用于计算两个数的商。"}
(同样,summary 和 JSON 在终端里各是一整行。)
这次模型先 cat 再 wc -l,说明准确了。小模型对「要先看内容」这类隐含步骤不敏感,在 Recipe 里把步骤写明白,比换参数更有效。
想在脚本里只取 JSON,可以用 grep 截出 JSON,再交给 jq 解析。下面这条管道没有整条实测;其中 grep 和 jq 两段在保存下来的上述输出上试过,得到 6:
goose run --recipe count-lines-v2.yaml --params file=calc.py 2>/dev/null \
| grep -o '{"file".*}' | jq .lines
第 6 步:`file` 类型参数和 `indent()` 过滤器
参数的 input_type 有六种:
| input_type | 含义 |
|---|---|
string | 默认类型,原样替换 |
number | 数字 |
boolean | true / false |
date | 日期,目前按文本处理 |
file | 传文件路径,goose 读出文件内容再替换进去 |
select | 下拉选择,必须配 options |
requirement 有三种:required(必填)、optional(可省略,必须有默认值)、user_prompt(没传时交互询问;无头运行时没传,占位符可能原样留下)。官方还规定 file 参数不能设默认值,防止误读敏感文件。
试一下 file 类型,先用最直观的写法:
cat > review-file.yaml <<'EOF'
version: "1.0.0"
title: "读文件内容"
description: "演示 file 类型参数"
parameters:
- key: src
input_type: file
requirement: required
description: "要分析的源码文件路径"
prompt: |
下面是源码,请指出潜在的 bug:
{{ src }}
EOF
goose run --recipe review-file.yaml --params src=calc.py --render-recipe
实测渲染结果(节选 prompt):
prompt: |
下面是源码,请指出潜在的 bug:
def add(a, b):
return a + b
div 函数不见了,而 goose recipe validate review-file.yaml 照样显示 ✓ recipe file is valid(它检查的是替换前的文件)。原因是多行内容被原样贴进 YAML:只有第一行带了缩进,后面顶格的 def div 被当成了块外的内容。官方文档为此提供了 indent() 过滤器,把占位符改成 {{ src | indent(2) }}:
sed 's/ {{ src }}/ {{ src | indent(2) }}/' review-file.yaml \
> review-file-v2.yaml
goose run --recipe review-file-v2.yaml --params src=calc.py --render-recipe
prompt: |-
下面是源码,请指出潜在的 bug:
def add(a, b):
return a + b
def div(a, b):
return a / b
这次 6 行全部进来了。凡是把多行内容(文件、日志、JSON)替换进 prompt: | 块,都应加 indent(),数字填块内的缩进空格数,并先用 --render-recipe 检查。
第 7 步:列出、分享和定时运行
把 Recipe 所在目录加进 GOOSE_RECIPE_PATH,就能按名字列出:
cd /tmp && GOOSE_RECIPE_PATH=/tmp/gt3 goose recipe list
Available recipes:
review-file-v2 - 演示 file 类型参数 - local: /tmp/gt3/review-file-v2.yaml
count-lines - 统计指定文件的行数并给出一句话说明
- local: /tmp/gt3/count-lines.yaml
review-file - 演示 file 类型参数 - local: /tmp/gt3/review-file.yaml
count-lines-v2 - 统计指定文件的行数并给出一句话说明
- local: /tmp/gt3/count-lines-v2.yaml
(两条较长的记录在终端里各是一行,这里折行显示。)
校验失败的 bad.yaml 没有出现在列表里。
生成分享链接(实测,链接很长,这里只保留开头):
cd /tmp/gt3 && goose recipe deeplink count-lines-v2.yaml
✓ Generated deeplink for: 统计代码行数
goose://recipe?config=eyJ2ZXJzaW9uIjoiMS4wLjAiLCJ0aXRsZSI6...
goose:// 链接由 goose Desktop 打开。
下面是官方文档给出的定时任务命令,本课未实测(盒子里没有常驻的 goose 调度进程)。cron 表达式有 6 段,第一段是秒,下面表示每天 9:00:
goose schedule add --schedule-id daily-report \
--cron "0 0 9 * * *" --recipe-source ./recipes/daily-report.yaml
goose schedule list
goose schedule sessions --schedule-id daily-report -l 10
goose schedule run-now --schedule-id daily-report
goose schedule remove --schedule-id daily-report
同样仅依据官方文档、未实测:goose recipe open my-recipe.yaml 在 goose Desktop 中打开 Recipe;Recipe 的 retry 字段可以配置成功检查和自动重试;activities 字段只在 Desktop 中显示。
小结
- Recipe 必填
title、description,以及prompt或instructions;命令行用.yaml或.json,不用.yml; goose recipe validate校验,--explain查参数,--render-recipe预览替换结果,三者都不调用模型;--params key=value传参,可选参数必须有默认值;response.json_schema让结果以固定结构的 JSON 输出,提示词要把步骤写明白;- 多行内容替换进 YAML 块时加
indent()过滤器; GOOSE_RECIPE_PATH让goose recipe list按名字找到 Recipe,goose schedule可以定时运行。
三课学完,你已经能在命令行里用本地模型跑 goose,无人值守地执行任务、管理会话,并把常用流程固化成带参数、输出结构化结果的 Recipe。
