Goose 的 Recipe 自动生成提示词解析:recipe.md 如何把对话沉淀为可复用配方
【免费下载链接】goosean open source, extensible AI agent that goes beyond code suggestions - install, execute, edit, and test with any LLM项目地址: https://gitcode.com/GitHub_Trending/goose3/goose
在 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 自我沉淀工作流"实现方式的代表性切片。
【免费下载链接】goosean open source, extensible AI agent that goes beyond code suggestions - install, execute, edit, and test with any LLM项目地址: https://gitcode.com/GitHub_Trending/goose3/goose
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考