Goose 的 Recipe 自动生成提示词解析:recipe.md 如何把对话沉淀为可复用配方
在 goose 中,Recipe(配方)是把一段已验证有效的对话流程沉淀为可复用资产的核心机制。本文以 crates/goose/src/prompts/recipe.md 这份提示词模板为主体,完整拆解它对模型输出的结构化契约、运行时加载链路,以及 Agent::create_recipe 中从"对话历史"到"Recipe 对象"的全流程解析与降级策略。读完本文,你可以理解 goose 在什么时机调用这份提示词、模型必须返回什么样的 JSON、以及解析失败时系统如何兜底。
提示词原文与输出契约
recipe.md 是一份直接发给模型的"元提示词",要求模型基于"到目前为止的整个对话"生成一个配方定义。其完整原文如下:
Based on our conversation so far, could you create:
1. A concise title (5-10 words) that captures the main topic or task
2. A brief description (1-2 sentences) that summarizes what this recipe helps with
3. A concise set of instructions (1-2 paragraphs) that describe what you've been helping with.
Make the instructions generic, and higher-level so that can be re-used across various
similar tasks. Pay special attention if any output styles or formats are requested
(and make it clear), and note any non standard tools used or required.
4. A list of 3-5 example activities (as a few words each at most) that would be relevant
to this topic
Format your response in _VALID_ json, with keys being `title`, `description`,
`instructions` (string), and `activities` (array of strings).
For example, perhaps we have been discussing fruit and you might write:
{
"title": "Fruit Information Assistant",
"description": "A recipe for finding and sharing information about different types of fruit.",
"instructions": "Using web searches we find pictures of fruit, and always check what language to reply in.",
"activities": [
"Show pics of apples",
"say a random fruit",
"share a fruit fact"
]
}
这份提示词的设计要点可以归纳为一张输出契约表:
| 输出字段 | JSON 类型 | 提示词中的约束 | 对应的 Recipe 字段 |
|---|---|---|---|
title |
string | 5-10 个词,概括主题或任务 | Recipe.title(必填) |
description |
string | 1-2 句,说明该配方能帮到什么 | Recipe.description(必填) |
instructions |
string | 1-2 段;要求"泛化、高层"以便跨相似任务复用;必须显式保留用户要求的输出风格/格式,并注明使用到的非标准工具 | Recipe.instructions |
activities |
string[] | 3-5 个,每个仅几个词,作为加载配方时的活动建议(activity pills) | Recipe.activities |
两个字段名"泛化"是刻意为之的:instructions 不能只复述本次对话,而要写成可以在多个相似任务中重放的通用说明;文末的"水果助手"示例则用具体样例锚定了四个字段的长度、语气与 JSON 形状,降低模型跑偏的概率。
值得注意的是,契约中只要求这 4 个字段——version、settings、author、extensions 等其余 Recipe 字段不由模型生成,而是由宿主代码在解析后注入(后文详述),这缩小了模型出错的表面积。
提示词的注册与运行时加载
模板在 crates/goose/src/prompt_template.rs 中注册,描述为 "Prompt for generating recipe files from conversations":
(
"recipe.md",
"Prompt for generating recipe files from conversations",
),
加载入口是 PromptManager::get_recipe_prompt:
pub async fn get_recipe_prompt(&self) -> String {
let context: HashMap<&str, Value> = HashMap::new();
prompt_template::render_template("recipe.md", &context)
.unwrap_or_else(|_| "The recipe prompt is busted. Tell the user.".to_string())
}
两个实现细节值得注意:
- 空上下文渲染:
recipe.md不含任何模板变量(如{{...}}占位符),因此传入空的HashMap即可。这与同目录中需要注入工具列表的plan.md等模板形成对比——提示词本身与运行环境解耦,全部上下文来自调用方拼接的对话消息。 - 显式降级:渲染失败时不抛错,而是返回一句让 Agent 告知用户的兜底文案,保证"创建配方"这条交互路径不会因模板问题而硬失败。
create_recipe:从对话历史到模型调用
核心调用链在 Agent::create_recipe,按源码顺序分五步:
- 收集运行环境:从
session_id取出会话,读取该会话工作目录下的扩展信息(extensions_info)与模型配置(model_config),并用当前 goose_mode 构建系统提示词。 - 注入提示词与工具:
get_recipe_prompt()的文本被作为一条Message::user()消息追加到对话末尾;同时通过get_prefixed_tools拿到会话可用的工具集,并过滤掉is_tool_visible_to_model判定为不可见的工具——即模型在"写配方"时仍能看到当前会话用过的工具描述,这正是提示词要求"注明非标准工具"的底气所在。 - 裁剪对话历史:recipe_conversation_history 只保留
agent_visible_messages()并剔除is_turn_context()消息。源码注释说明了原因:"The recipe prompt has no turn-context instructions; drop the blocks."——recipe.md 是纯抽取型提示词,不承载轮次上下文语义,多余的 turn-context 块只会干扰抽取。 - 消息修复:依次执行
fix_conversation(问题逐条tracing::warn!记录)与merge_consecutive_messages_for_request,保证送模型的消息序列合法。 - 单次补全:调用
provider.complete(&model_config, &system_prompt, messages, &tools)一次性拿到结果文本,不做流式。
模型输出的解析:JSON 优先,字符串解析兜底
模型返回的文本在 agent.rs 中经过两级解析。
第一级:剥离代码围栏并解析 JSON
提示词虽要求"VALID json",但模型仍可能把结果包进 Markdown 代码块,因此先做围栏剥离:
// the response may be contained in ```json ```, strip that before parsing json
let re = Regex::new(r"(?s)```[^\n]*\n(.*?)\n```").unwrap();
let clean_content = re
.captures(&content)
.and_then(|caps| caps.get(1).map(|m| m.as_str()))
.unwrap_or(&content)
.trim()
.to_string();
随后对 clean_content 做 serde_json::from_str::<Value>,并按契约严格校验:
instructions必须存在且为字符串,否则报Missing 'instructions' in json response/instructions' is not a string;activities必须存在且为数组,且每个元素必须是字符串,逐个map校验。
title 与 description 的提取相对宽松:解析成功但字段缺失或类型不符时,分别回落到默认值 "Custom recipe from chat" 与 "a custom recipe instance from this chat session"(agent.rs#L4174-L4194)。
第二级:纯文本降级
若 JSON 解析整体失败,源码进入降级分支(agent.rs#L4111-L4143):
- 用
split_once("instructions:")截取 instructions 之后的内容; - 再
split_once("activities:")切出活动列表; - 对活动列表逐行用正则
^[•\-*\d]+\.?\s*去掉项目符号(圆点、星号、短横线或数字编号),过滤空行。
这条降级路径说明 goose 对该提示词的健壮性假设是"模型大概率给 JSON,但给不了也不能让功能不可用"。
宿主侧字段注入:从 4 字段 JSON 到完整 Recipe
JSON 解析出的 4 个字段并不是最终形态。agent.rs#L4145-L4211 继续注入宿主持有的字段:
- extensions:
get_enabled_extensions()取当前启用的扩展配置写入extensions; - author:从环境变量
USER(或USERNAME)读取作为contact; - settings:由全局配置回填
goose_provider(config.get_goose_provider(),未配置时直接报No provider configured. Run 'goose configure' first)、goose_model(当前会话模型名)、temperature(model_config.temperature缺省为0.0)。
最终经 Recipe::builder() 构建:
let recipe = Recipe::builder()
.title(title)
.description(description)
.instructions(instructions)
.activities(activities)
.extensions(extension_configs)
.settings(settings)
.author(author)
.build()
RecipeBuilder::build() 的校验规则(mod.rs#L409-L415)与提示词契约呼应:title、description 必填,且 prompt 与 instructions 至少其一非空——本流程中 instructions 恒由提示词产出,故契约天然满足。
生成结果的去向:Recipe 如何被消费
自动生成的 Recipe 与普通手写配方走完全相同的加载与模板渲染管线:
- Recipe::from_content 支持 YAML/JSON 双格式,并兼容顶层嵌套
recipe:键的写法;解析后还会自动注入依赖扩展(如含sub_recipes时自动补summon扩展)。 - 在 CLI 路径中,extract_recipe_info_from_cli 展示了字段语义的最终落点:
recipe.prompt成为会话的首条用户输入(InputConfig.contents),recipe.instructions成为附加系统提示(additional_system_prompt),参数经 build_recipe_from_template 渲染模板中的{{param}}占位符。 - 而提示词契约中的
activities正是加载配方时界面上"活动建议"(activity pills)的数据来源——这也解释了为何提示词特别强调 activities 要"每个仅几个词":它们是要直接展示给用户点击的短标签。
小结
recipe.md 看似只有一页,却是 goose"对话资产化"闭环的起点:它用一份带完整示例的输出契约(4 字段、限定长度、泛化要求、工具声明要求)把开放式对话压缩为结构化元数据;get_recipe_prompt 保证模板可安全加载;create_recipe 则用"代码围栏剥离 + JSON 校验 + 字符串降级"的三级解析把模型输出变成可信的 Recipe 对象,再由宿主注入 provider、model、extensions 等运行字段,使自动生成的配方与手写配方等价可运行。对阅读 goose 源码的开发者而言,这条链路是理解"Agent 自我沉淀工作流"实现方式的代表性切片。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00