第 3 课

Recipe:带参数的可复用任务与结构化 JSON 输出

写一份带必填、可选参数和 response.json_schema 的 Recipe,用 recipe validate、--explain、--render-recipe 校验和预览,运行拿到固定结构的 JSON 并通过改提示词修正结果,再实测 file 类型参数的多行缩进坑与 indent() 解法,最后用 GOOSE_RECIPE_PATH、deeplink 和 schedule 管理与定时运行。

图文24 分钟goose 官方文档 ↗

课程目录第 3 / 3 课

学习位置仅保存在当前浏览器,有效期 180 天。

01 / 图文教材

图文教材

本课依据: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数字
booleantrue / 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。