diff --git a/docs/features/knowledge-base/README.md b/docs/features/knowledge-base/README.md index 9c6c3729..895fb1cd 100644 --- a/docs/features/knowledge-base/README.md +++ b/docs/features/knowledge-base/README.md @@ -31,6 +31,13 @@ FastGPT/RAGFlow 完整 App 与专属配置、真实远程预检索仍待验收 | [2026-09-13 产品验证摘要](./external-knowledge-validation-2026-09-13.json) | 生产服务与本机 HTTP MCP、Dify 桌面、凭据重开、调用计数、最终自动检查状态及远程阻断的脱敏证据 | | [2026-09-13 UI 与真实生成复验](./external-knowledge-follow-up-2026-09-13.json) | 后续 UI 修正、三家生产 IPC 生成、Dify 完整 App 回答与引用、视觉 fixture 及本轮请求计数 | +## 模型图谱抽取 + +图谱抽取不设置独立的输出 token 预算。OpenAI Chat Completions 和 Responses +请求省略输出上限参数,由模型服务决定;Anthropic 必填的 `max_tokens` 沿用默认文本 +模型连接的最大输出配置,未配置时使用应用共享协议默认值,不再固定为 8192。 +服务端仍可能因自身限制返回长度截断;既有超时、取消、响应大小边界和失败诊断保持不变。 + ## 术语 - **外部实例**:一个由用户配置的 Dify、FastGPT 或 RAGFlow 服务地址及其认证信息。 diff --git a/src/main/knowledge/model-extractor.test.ts b/src/main/knowledge/model-extractor.test.ts index c14d907d..9cecd220 100644 --- a/src/main/knowledge/model-extractor.test.ts +++ b/src/main/knowledge/model-extractor.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest' +import { defaultAnthropicMaximumOutputTokens } from '../../shared/contracts' import type { ResolvedRuntimeSettings, RuntimeSettingsStore @@ -14,6 +15,8 @@ function store( modelName: 'intranet-model', modelProtocol: 'anthropic-messages', modelAuthentication: 'none', + modelProfiles: [], + defaultModelProfileId: 'default-model', ...overrides } as ResolvedRuntimeSettings return { @@ -29,6 +32,70 @@ function jsonResponse(payload: unknown, status = 200): Response { } describe('createModelGraphExtractor', () => { + it.each([ + 'openai-chat-completions', + 'openai-responses' + ] as const)('does not impose an output budget for %s', async (protocol) => { + const fetcher = vi.fn(async () => + jsonResponse({ + choices: [{ message: { content: '{"entities":[]}' } }], + output_text: '{"entities":[]}' + }) + ) + const extract = createModelGraphExtractor( + store({ + modelProtocol: protocol, + modelProfiles: [{ + id: 'default-model', + name: 'Default model', + baseUrl: 'http://localhost:8000', + modelName: 'intranet-model', + protocol, + authentication: 'none', + maximumOutputTokens: 8192 + }] + }), + fetcher + ) + + await expect(extract('extract this')).resolves.toEqual({ entities: [] }) + const body = JSON.parse(String(fetcher.mock.calls[0]?.[1]?.body)) + expect(body).not.toHaveProperty('max_tokens') + expect(body).not.toHaveProperty('max_completion_tokens') + expect(body).not.toHaveProperty('max_output_tokens') + }) + + it.each([undefined, 48_000])( + 'uses the Anthropic connection protocol limit (%s), not an extraction budget', + async (maximumOutputTokens) => { + const fetcher = vi.fn(async () => + jsonResponse({ content: [{ type: 'text', text: '{"entities":[]}' }] }) + ) + const profile = { + name: 'Model', + baseUrl: 'http://localhost:8000', + modelName: 'intranet-model', + protocol: 'anthropic-messages' as const, + authentication: 'none' as const + } + const extract = createModelGraphExtractor( + store({ + modelProfiles: [ + { ...profile, id: 'other-model', maximumOutputTokens: 1024 }, + { ...profile, id: 'default-model', maximumOutputTokens } + ] + }), + fetcher + ) + + await expect(extract('extract this')).resolves.toEqual({ entities: [] }) + const body = JSON.parse(String(fetcher.mock.calls[0]?.[1]?.body)) + expect(body.max_tokens).toBe( + maximumOutputTokens ?? defaultAnthropicMaximumOutputTokens + ) + } + ) + it('uses an unauthenticated Anthropic endpoint with its path and query', async () => { const fetcher = vi.fn(async () => jsonResponse({ diff --git a/src/main/knowledge/model-extractor.ts b/src/main/knowledge/model-extractor.ts index 8649ff20..a448295f 100644 --- a/src/main/knowledge/model-extractor.ts +++ b/src/main/knowledge/model-extractor.ts @@ -1,4 +1,5 @@ import type { RuntimeSettingsStore } from '../runtime-settings-store' +import { defaultAnthropicMaximumOutputTokens } from '../../shared/contracts' import { createOpenAIChatCompletionsUrl, createOpenAIResponsesUrl @@ -208,7 +209,6 @@ export function createModelGraphExtractor( protocol === 'openai-responses' ? { model: settings.modelName, - max_output_tokens: 8192, stream: false, instructions: system, input: userPrompt @@ -216,14 +216,17 @@ export function createModelGraphExtractor( : protocol === 'anthropic-messages' ? { model: settings.modelName, - max_tokens: 8192, + // Anthropic requires this field; reuse the connection's protocol limit. + max_tokens: + settings.modelProfiles.find( + (profile) => profile.id === settings.defaultModelProfileId + )?.maximumOutputTokens ?? defaultAnthropicMaximumOutputTokens, stream: false, system, messages: [{ role: 'user', content: userPrompt }] } : { model: settings.modelName, - max_tokens: 8192, stream: false, messages: [ { role: 'system', content: system }, diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 2560c69a..d3a22be4 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -11670,12 +11670,14 @@ function App(): React.JSX.Element {
)} @@ -911,6 +913,9 @@ function ChatMessageRowView({ aria-label={t('chat.images.downloadNamed', { title: artifact.title })} + className="icon-button attachment-action" + title={t('chat.images.downloadImage')} + data-tooltip={t('chat.images.downloadImage')} onClick={() => onDownloadImage({ src: artifact.content!, @@ -919,8 +924,7 @@ function ChatMessageRowView({ } type="button" > - - {t('chat.images.download')} +