跳到主要内容

02 · Prompt 设计与结构化输出

这章讲:项目里 5 个 LLM 调用点各自的 prompt 长啥样、想拿什么,以及它们共用的"用 generateObject + zod schema 逼模型吐结构化 JSON"的套路和统一人格。

3.6 共同套路:generateObject + zod

要解决的小问题: LLM 默认吐自由文本,但这个 agent 每一步都要"可编程地"拿到结果——一个搜索词数组、一个 learnings 数组。解析自由文本很脆。

思路: 用 Vercel ai SDK 的 generateObject,把"我要什么形状"用 zod schema 声明出来。SDK 会把 schema 转成模型的结构化输出约束(或函数调用),保证返回的 res.object 就是那个形状。而且 zod 每个字段的 .describe(...) 会作为提示塞给模型——schema 既是校验器,也是 prompt 的一部分

// 示意,非源码:套路模板
const res = await generateObject({
model: getModel(),
system: systemPrompt(), // 统一人格
prompt: `...任务描述...`,
schema: z.object({
queries: z.array(z.object({
query: z.string().describe('The SERP query'), // 字段描述=给模型的微提示
})),
}),
});
res.object.queries; // 直接拿到强类型数组

五个调用点全都是这个模板,只是 prompt 文本和 schema 不同。

3.7 统一人格 systemPrompt

所有调用共享同一个 system 提示(src/prompt.ts:1-15)。它做两件事:注入当天日期new Date().toISOString(),让模型知道"今天",好处理时效信息),和设定人格——一串"你是专家研究员/把我当专家/别简化/重视论证胜过权威/允许推测但要标注"的指令:

export const systemPrompt = () => {
const now = new Date().toISOString();
return `You are an expert researcher. Today is ${now}. Follow these instructions when responding:
- You may be asked to research subjects that is after your knowledge cutoff, assume the user is right when presented with news.
...`;
};

注意"assume the user is right when presented with news"这条——它专门为"研究超过模型知识截止日的新事件"松绑,避免模型嘴硬否认新闻。

3.8 五个调用点各自要什么

调用点输入schema 产出文件
generateFeedback原始题目questions[] 澄清问题src/feedback.ts:7
generateSerpQueries题目 + 已有 learningsqueries[]query + researchGoalsrc/deep-research.ts:40
processSerpResult搜索词 + 网页正文learnings[] + followUpQuestions[]src/deep-research.ts:81
writeFinalReport题目 + 全部 learningsreportMarkdown 长报告src/deep-research.ts:120
writeFinalAnswer题目 + 全部 learningsexactAnswer 一句话答案src/deep-research.ts:149

想词 generateSerpQueries — 让搜索词自带"研究目标"

最巧的是它不只要搜索词,还让模型为每个词写一段 researchGoalsrc/deep-research.ts:66-70):

researchGoal: z.string().describe(
'First talk about the goal of the research that this query is meant to accomplish, then go deeper into how to advance the research once the results are found, mention additional research directions. Be as specific as possible, especially for additional research directions.',
),

这段 researchGoal 正是第 01 章里"往下一层的题目"的来源——模型自己规划了下钻方向。prompt 里还会把上一轮的 learnings 拼进去(:54-60),实现"顺着已知往更具体的方向搜"。

提炼 processSerpResult — 逼出"信息密度"

prompt(src/deep-research.ts:102)明确要求 learnings 要"concise、information dense、包含人名/地点/公司/产品/确切指标/数字/日期"。这是刻意的:learnings 是要喂回下一轮和最终报告的"压缩燃料",越密越好。它同时产出 followUpQuestions,数量由上层传下来的 numFollowUpQuestions(=下一层 breadth)决定。

喂进去的网页正文先被 trimPrompt(content, 25_000) 逐条截到 25k token(:92-94),再用 <content>...</content> XML 标签包裹拼接——用 XML 标签分隔多段内容是全项目一致的习惯(<prompt><query><learning><content> 到处可见),帮模型分清边界。

写报告 vs 写答案 — 一体两面

两者结构对称,只是目标相反:

  • writeFinalReport:120)要"aim for 3 or more pages, include ALL the learnings",产出长 Markdown;之后代码手工visitedUrls 拼成 ## Sources 附在报告尾部(src/deep-research.ts:144-146)——来源列表不经过 LLM,保证不丢链接。
  • writeFinalAnswer:149)反过来要"Do not yap or babble",只吐符合题目格式的极简答案(用于评测/精确问答场景)。

3.9 关键细节 / 坑

.slice(0, numQueries) 双保险。 generateSerpQueriesgenerateFeedback 都在 schema 里说"max N",返回后又 .slice(0, N) 硬截一刀(:78feedback.ts:27)——不信任模型一定守数量约束。

提炼调用带 60 秒超时。 processSerpResult 传了 abortSignal: AbortSignal.timeout(60_000):99),防止某次 LLM 调用卡死拖住整支递归;超时会被第 01 章的 try/catch 接住。

schema 描述里回填数量。`List of learnings, max of ${numLearnings}` 这种把运行时变量插进 .describe(),让"最多几条"这个约束同时出现在 prompt 语义和字段描述里,双重提醒模型。

下一章:这些调用背后的模型从哪来、prompt 太长怎么办 → 03-providers-and-trimming.md