Appearance
nextTurnParams 是 OpenRouter Agent SDK 中工具(Tool)的一个特殊字段,允许工具在 execute 之后、模型接收结果之前,动态修改下一次 callModel 调用的任意参数(instructions、model、input、temperature 等)。这一机制是实现 Skills 系统、上下文渐进积累、按复杂度自动选模型等高阶 Agent 模式的核心。
为什么需要 nextTurnParams?
传统工具调用只能把结果返回给模型,但有时候你需要的不止于此:
- Skills/Plugins:激活某个技能时,将其说明文件注入到对话上下文中
- 渐进式上下文:每次工具调用都向对话历史追加新的背景信息
- 自适应行为:根据工具结果动态调整模型参数
- 关注点分离:工具自己管理所需的上下文,调用方无需关心
基础示例
typescript
import { tool } from '@openrouter/agent';
import { z } from 'zod';
const expertModeTool = tool({
name: 'enable_expert_mode',
description: 'Enable expert mode for detailed technical responses',
inputSchema: z.object({
domain: z.string().describe('Technical domain (e.g., "kubernetes", "react")'),
}),
outputSchema: z.object({ enabled: z.boolean() }),
nextTurnParams: {
instructions: (params, context) => {
const base = context.instructions ?? '';
return `${base}
EXPERT MODE ENABLED for ${params.domain}:
- Provide detailed technical explanations
- Include code examples and best practices
- Reference official documentation
- Assume advanced knowledge`;
},
temperature: () => 0.3, // 技术回答需要更精确
},
execute: async (params) => {
return { enabled: true };
},
});Claude Code Skills 模式复现
用一个封装好的工具复现 Claude Code 的 Skills 系统:
typescript
import { tool } from '@openrouter/agent';
import { readFileSync } from 'fs';
import { z } from 'zod';
const skillsTool = tool({
name: "skill",
description: `Load a specialized skill to enhance the assistant's capabilities.
Available skills: pdf-processing, data-analysis, code-review, etc.
Each skill provides domain-specific instructions and capabilities.`,
inputSchema: z.object({
type: z.string().describe("The skill type to load (e.g., 'pdf-processing')"),
}),
outputSchema: z.string(),
// nextTurnParams 在所有工具调用执行完毕后、结果发给模型之前运行
// 按 tools 数组顺序执行,魔法就发生在这里
nextTurnParams: {
input: (params, context) => {
// 幂等性检查:防止重复加载同一个 skill
if (JSON.stringify(context.input).includes(`Skill ${params.type} is already loaded`)) {
return context.input;
}
// 从文件系统读取 skill 说明文件
const skill = readFileSync(
`~/.claude/skills/${params.type}/SKILL.md`,
"utf-8"
);
// 将 skill 上下文注入对话历史
return [
...context.input,
{
role: "user",
content: `Base directory for this skill: ~/.claude/skills/${params.type}/
${skill}`,
},
];
},
},
execute: async (params, context) => {
if (JSON.stringify(context.input).includes(`Skill ${params.type} is already loaded`)) {
return `Skill ${params.type} is already loaded`;
}
return `Launching skill ${params.type}`;
},
});
// 使用:skill 会自动在后续对话中注入专属指令
const result = openrouter.callModel({
model: 'anthropic/claude-sonnet-4.5',
input: 'Process this PDF and extract the key findings',
tools: [skillsTool],
});核心优势
- 封装性:skill 加载逻辑完全内聚在工具里
- 幂等性:内置检查防止同一 skill 重复加载
- 干净的 API:调用方不需要知道 skill 文件在哪
- 可组合:多个 skill 可以跨轮次依次加载
可用上下文(context 对象)
nextTurnParams 的每个函数接收两个参数:params(工具输入)和 context(当前请求上下文):
| 属性 | 类型 | 说明 |
|---|---|---|
input | OpenResponsesInput | 当前消息历史 |
model | string | undefined | 当前模型 |
models | string[] | undefined | 模型备选数组 |
instructions | string | undefined | 当前系统指令 |
temperature | number | undefined | 当前 temperature |
maxOutputTokens | number | undefined | 当前最大 token 数 |
topP | number | undefined | 当前 top-p |
topK | number | undefined | 当前 top-k |
可修改的参数
nextTurnParams 支持修改 CallModelInput 的任意字段:
typescript
nextTurnParams: {
input: (params, ctx) => [...ctx.input, newMessage], // 修改消息历史
model: (params, ctx) => 'anthropic/claude-sonnet-4.5', // 切换模型
instructions: (params, ctx) => `${ctx.instructions}\n\nNew context...`, // 追加指令
temperature: (params, ctx) => 0.5, // 调整参数
maxOutputTokens: (params, ctx) => 2000,
},实用模式
研究上下文渐进积累
随着工具被调用,逐步向模型注入研究背景:
typescript
const researchTool = tool({
name: "research",
inputSchema: z.object({ topic: z.string() }),
outputSchema: z.object({ findings: z.array(z.string()) }),
nextTurnParams: {
instructions: (params, context) => {
const base = context.instructions ?? '';
return `${base}
Previous research on "${params.topic}" found important context.
Build upon these findings in your response.`;
},
},
execute: async (params) => {
const results = await searchDatabase(params.topic);
return { findings: results };
},
});按复杂度自动升级模型
分析代码复杂度后切换到更强的模型:
typescript
const complexityAnalyzer = tool({
name: "analyze_complexity",
inputSchema: z.object({ code: z.string() }),
outputSchema: z.object({ complexity: z.enum(['low', 'medium', 'high']) }),
nextTurnParams: {
model: (params, context) => {
if (params.complexity === 'high') {
return 'anthropic/claude-sonnet-4.5'; // 复杂任务升级模型
}
return context.model ?? 'openai/gpt-5-nano';
},
temperature: (params, context) => {
return params.complexity === 'high' ? 0.3 : 0.7; // 复杂任务降低随机性
},
},
execute: async (params) => {
return analyzeCodeComplexity(params.code);
},
});批量加载多个 Skill
一次性加载多个 skill 到对话上下文:
typescript
const multiSkillLoader = tool({
name: 'load_skills',
description: 'Load multiple skills at once',
inputSchema: z.object({
skills: z.array(z.string()).describe('Array of skill names to load'),
}),
outputSchema: z.object({
loaded: z.array(z.string()),
failed: z.array(z.object({ name: z.string(), reason: z.string() })),
}),
nextTurnParams: {
input: (params, context) => {
let newInput = context.input;
for (const skillName of params.skills) {
const skillPath = `~/.skills/${skillName}/SKILL.md`;
if (!existsSync(skillPath)) continue;
const skillMarker = `[Skill: ${skillName}]`;
if (JSON.stringify(newInput).includes(skillMarker)) continue;
const skillContent = readFileSync(skillPath, 'utf-8');
newInput = [
...(Array.isArray(newInput) ? newInput : [newInput]),
{ role: 'user', content: `${skillMarker}\n${skillContent}` },
];
}
return newInput;
},
},
execute: async ({ skills }) => {
const loaded = [];
const failed = [];
for (const skill of skills) {
if (existsSync(`~/.skills/${skill}/SKILL.md`)) {
loaded.push(skill);
} else {
failed.push({ name: skill, reason: 'Not found' });
}
}
return { loaded, failed };
},
});用户语言偏好适配
typescript
const languageTool = tool({
name: 'set_language',
inputSchema: z.object({
language: z.enum(['en', 'es', 'fr', 'de', 'ja']),
}),
outputSchema: z.object({ set: z.boolean() }),
nextTurnParams: {
instructions: (params, context) => {
const base = context.instructions ?? '';
const instructions: Record<string, string> = {
en: 'Respond in English.',
es: 'Responde en español.',
fr: 'Répondez en français.',
de: 'Antworten Sie auf Deutsch.',
ja: '日本語で回答してください。',
};
return `${base}\n\n${instructions[params.language]}`;
},
},
execute: async (params) => ({ set: true }),
});最佳实践
幂等性检查
向上下文追加内容前,先检查是否已经存在:
typescript
nextTurnParams: {
input: (params, context) => {
const marker = `[Context: ${params.id}]`;
if (JSON.stringify(context.input).includes(marker)) {
return context.input; // 已存在,直接返回不修改
}
return [...context.input, { role: 'user', content: `${marker}\n${newContent}` }];
},
},类型安全的 context 访问
使用可选链和 fallback 值避免空指针:
typescript
nextTurnParams: {
instructions: (params, context) => {
const base = context.instructions ?? 'You are a helpful assistant.';
return `${base}\n\nAdditional context: ${params.data}`;
},
},最小化修改原则
只修改必要的参数,不要无端改动其他值:
typescript
// 好:精准修改
nextTurnParams: {
temperature: (params) => params.needsPrecision ? 0.2 : undefined,
},
// 避免:把 context 的值原样传回去(无意义的操作)
nextTurnParams: {
temperature: (params, ctx) => {
return params.needsPrecision ? 0.2 : ctx.temperature;
},
},常见问题
Q: nextTurnParams 和 execute 的执行顺序是什么?
A: execute 先运行,完成后所有工具的 nextTurnParams 按照 tools 数组的顺序依次执行,最后才把工具结果和修改后的参数一起发给模型。
Q: 多个工具都定义了 nextTurnParams.instructions,会怎么样?
A: 它们会按顺序链式叠加。每个函数接收的 context.instructions 是上一个函数的返回值,所以每个工具都需要将 base 包含在返回值里,而不是直接替换。
Q: nextTurnParams 修改了 model,下一轮之后还会恢复吗?
A: 不会自动恢复,修改会一直持续。如果只想对某一轮生效,需要在该工具或后续工具的 nextTurnParams 里把模型改回去。