Skip to content

nextTurnParams 是 OpenRouter Agent SDK 中工具(Tool)的一个特殊字段,允许工具在 execute 之后、模型接收结果之前,动态修改下一次 callModel 调用的任意参数(instructionsmodelinputtemperature 等)。这一机制是实现 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],
});

核心优势

  1. 封装性:skill 加载逻辑完全内聚在工具里
  2. 幂等性:内置检查防止同一 skill 重复加载
  3. 干净的 API:调用方不需要知道 skill 文件在哪
  4. 可组合:多个 skill 可以跨轮次依次加载

可用上下文(context 对象)

nextTurnParams 的每个函数接收两个参数:params(工具输入)和 context(当前请求上下文):

属性类型说明
inputOpenResponsesInput当前消息历史
modelstring | undefined当前模型
modelsstring[] | undefined模型备选数组
instructionsstring | undefined当前系统指令
temperaturenumber | undefined当前 temperature
maxOutputTokensnumber | undefined当前最大 token 数
topPnumber | undefined当前 top-p
topKnumber | 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: nextTurnParamsexecute 的执行顺序是什么?

A: execute 先运行,完成后所有工具的 nextTurnParams 按照 tools 数组的顺序依次执行,最后才把工具结果和修改后的参数一起发给模型。

Q: 多个工具都定义了 nextTurnParams.instructions,会怎么样?

A: 它们会按顺序链式叠加。每个函数接收的 context.instructions 是上一个函数的返回值,所以每个工具都需要将 base 包含在返回值里,而不是直接替换。

Q: nextTurnParams 修改了 model,下一轮之后还会恢复吗?

A: 不会自动恢复,修改会一直持续。如果只想对某一轮生效,需要在该工具或后续工具的 nextTurnParams 里把模型改回去。