首页
/ Goose 的 Recipe 自动生成提示词解析:recipe.md 如何把对话沉淀为可复用配方

Goose 的 Recipe 自动生成提示词解析:recipe.md 如何把对话沉淀为可复用配方

2026-09-06 10:09:26作者:仰钰奇

在 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 个字段——versionsettingsauthorextensions 等其余 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,按源码顺序分五步:

  1. 收集运行环境:从 session_id 取出会话,读取该会话工作目录下的扩展信息(extensions_info)与模型配置(model_config),并用当前 goose_mode 构建系统提示词。
  2. 注入提示词与工具get_recipe_prompt() 的文本被作为一条 Message::user() 消息追加到对话末尾;同时通过 get_prefixed_tools 拿到会话可用的工具集,并过滤掉 is_tool_visible_to_model 判定为不可见的工具——即模型在"写配方"时仍能看到当前会话用过的工具描述,这正是提示词要求"注明非标准工具"的底气所在。
  3. 裁剪对话历史recipe_conversation_history 只保留 agent_visible_messages() 并剔除 is_turn_context() 消息。源码注释说明了原因:"The recipe prompt has no turn-context instructions; drop the blocks."——recipe.md 是纯抽取型提示词,不承载轮次上下文语义,多余的 turn-context 块只会干扰抽取。
  4. 消息修复:依次执行 fix_conversation(问题逐条 tracing::warn! 记录)与 merge_consecutive_messages_for_request,保证送模型的消息序列合法。
  5. 单次补全:调用 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_contentserde_json::from_str::<Value>,并按契约严格校验:

  • instructions 必须存在且为字符串,否则报 Missing 'instructions' in json response / instructions' is not a string
  • activities 必须存在且为数组,且每个元素必须是字符串,逐个 map 校验。

titledescription 的提取相对宽松:解析成功但字段缺失或类型不符时,分别回落到默认值 "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 继续注入宿主持有的字段:

  • extensionsget_enabled_extensions() 取当前启用的扩展配置写入 extensions
  • author:从环境变量 USER(或 USERNAME)读取作为 contact
  • settings:由全局配置回填 goose_providerconfig.get_goose_provider(),未配置时直接报 No provider configured. Run 'goose configure' first)、goose_model(当前会话模型名)、temperaturemodel_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)与提示词契约呼应:titledescription 必填,且 promptinstructions 至少其一非空——本流程中 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 自我沉淀工作流"实现方式的代表性切片。

登录后查看全文
热门项目推荐
相关项目推荐