Прошлые размышления модели не уходят в историю: фикс ошибок Gemini
- ProviderHistoryCompat с правилом strip-past-reasoning: из прошлых реплик вырезаются reasoning-части и подписи (reasoning_details), текущий ответ не трогается — Gemini нужны подписи внутри цикла - Причина: подписи Gemini 3 привязаны к исходному контексту, а из истории вырезаются вызовы инструментов и в треде смешаны ответы разных моделей — провайдер отвечал ошибкой (finishReason other/error) - Пустые реплики ассистента из упавших запросов тоже не отправляются - onError: в лог пишется текст ошибки провайдера Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { Agent } from "@mastra/core/agent";
|
||||
import { ToolCallFilter } from "@mastra/core/processors";
|
||||
import { ProviderHistoryCompat, ToolCallFilter } from "@mastra/core/processors";
|
||||
import { RequestContext } from "@mastra/core/request-context";
|
||||
import type { Memory } from "@mastra/memory";
|
||||
import { readPrompt } from "../../libs/prompts.js";
|
||||
import type { Persona } from "../../types.js";
|
||||
import type { ChatHistoryService } from "../chat-history-service.js";
|
||||
import type { ChatMemberService } from "../chat-member-service.js";
|
||||
import { stripPastReasoning } from "./history-compat.js";
|
||||
import { chatThreadId } from "./memory.js";
|
||||
import { buildInstructions, buildUserText, type Question } from "./prompts.js";
|
||||
import { createListChatMembersTool } from "./tools/list-chat-members-tool.js";
|
||||
@@ -69,10 +70,13 @@ export const createAssistantService = ({
|
||||
// На последнем шаге инструменты запрещены: иначе модель может потратить все шаги на чтение архива
|
||||
// и так и не ответить (в ответе бота было бы пусто).
|
||||
prepareStep: ({ stepNumber }) => (stepNumber >= MAX_AGENT_STEPS - 1 ? { toolChoice: "none" } : undefined),
|
||||
// Провайдеры (OpenRouter/Gemini) присылают ошибку частью ответа — без этого в логе только «finishReason: other».
|
||||
onError: ({ error }) => console.error("Ошибка провайдера модели:", error),
|
||||
},
|
||||
// Результаты инструментов (сотни сообщений архива) нужны только в том ответе, где их запросили:
|
||||
// из прошлых реплик их убираем. Внутри ответа не трогаем — иначе модель забывает прочитанное и читает снова.
|
||||
inputProcessors: [new ToolCallFilter()],
|
||||
// Прошлые размышления модели тоже убираем (см. history-compat.ts) — иначе Gemini ломается на старых подписях.
|
||||
inputProcessors: [new ToolCallFilter(), new ProviderHistoryCompat({ additionalRules: [stripPastReasoning] })],
|
||||
tools: {
|
||||
readChatMessages: createReadChatMessagesTool(chatHistoryService),
|
||||
listChatMembers: createListChatMembersTool(chatMemberService),
|
||||
|
||||
38
src/services/assistant/history-compat.ts
Normal file
38
src/services/assistant/history-compat.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { CompatRule } from "@mastra/core/processors";
|
||||
|
||||
type Prompt = Parameters<NonNullable<CompatRule["applyToPrompt"]>>[0]["prompt"];
|
||||
|
||||
type PromptMessage = Prompt[number];
|
||||
|
||||
/** Реплика ассистента из прошлого ответа без размышлений и их подписей (`providerOptions.openrouter.reasoning_details`). */
|
||||
const withoutReasoning = (message: PromptMessage): PromptMessage[] => {
|
||||
if (message.role !== "assistant") return [message];
|
||||
|
||||
const { providerOptions: _messageOptions, ...rest } = message;
|
||||
|
||||
const content = rest.content
|
||||
.filter((part) => part.type !== "reasoning")
|
||||
.map(({ providerOptions: _partOptions, ...part }) => part);
|
||||
|
||||
return content.length > 0 ? [{ ...rest, content }] : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Правило для Mastra `ProviderHistoryCompat`: из прошлых ответов убираем размышления модели.
|
||||
*
|
||||
* Зачем: Gemini 3 подписывает размышления (`reasoning.encrypted`), и подпись привязана к исходному контексту.
|
||||
* Мы вырезаем из истории вызовы инструментов (`ToolCallFilter`), а в одном треде бывают ответы разных моделей —
|
||||
* старые подписи перестают сходиться, и провайдер отвечает ошибкой. Для ответа прошлые размышления не нужны,
|
||||
* а токены на них тратятся. Текущий ответ (всё после последней реплики пользователя) не трогаем — там подписи
|
||||
* нужны Gemini для вызовов инструментов внутри цикла.
|
||||
*/
|
||||
export const stripPastReasoning: CompatRule = {
|
||||
name: "strip-past-reasoning",
|
||||
applyToPrompt: ({ prompt }) => {
|
||||
const currentTurnStart = prompt.findLastIndex((message) => message.role === "user");
|
||||
|
||||
if (currentTurnStart <= 0) return undefined;
|
||||
|
||||
return [...prompt.slice(0, currentTurnStart).flatMap(withoutReasoning), ...prompt.slice(currentTurnStart)];
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user