Прошлые размышления модели не уходят в историю: фикс ошибок 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 { 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 { RequestContext } from "@mastra/core/request-context";
|
||||||
import type { Memory } from "@mastra/memory";
|
import type { Memory } from "@mastra/memory";
|
||||||
import { readPrompt } from "../../libs/prompts.js";
|
import { readPrompt } from "../../libs/prompts.js";
|
||||||
import type { Persona } from "../../types.js";
|
import type { Persona } from "../../types.js";
|
||||||
import type { ChatHistoryService } from "../chat-history-service.js";
|
import type { ChatHistoryService } from "../chat-history-service.js";
|
||||||
import type { ChatMemberService } from "../chat-member-service.js";
|
import type { ChatMemberService } from "../chat-member-service.js";
|
||||||
|
import { stripPastReasoning } from "./history-compat.js";
|
||||||
import { chatThreadId } from "./memory.js";
|
import { chatThreadId } from "./memory.js";
|
||||||
import { buildInstructions, buildUserText, type Question } from "./prompts.js";
|
import { buildInstructions, buildUserText, type Question } from "./prompts.js";
|
||||||
import { createListChatMembersTool } from "./tools/list-chat-members-tool.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),
|
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: {
|
tools: {
|
||||||
readChatMessages: createReadChatMessagesTool(chatHistoryService),
|
readChatMessages: createReadChatMessagesTool(chatHistoryService),
|
||||||
listChatMembers: createListChatMembersTool(chatMemberService),
|
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