feat: 完善问问微信查询理解与检索链路

This commit is contained in:
Wxw-Gu
2026-09-09 16:51:31 +08:00
parent 7554de7d72
commit facc292443
14 changed files with 1107 additions and 66 deletions
@@ -236,6 +236,9 @@ export function AISearchWorkspace({
range,
timeRangeOverride,
activeContactMd5: activeContact?.md5,
knowledgeGeneration: knowledgeStatus
? `${knowledgeStatus.state}:${knowledgeStatus.indexedMessageCount}:${knowledgeStatus.indexedChunkCount}:${knowledgeStatus.processedMessages}`
: undefined,
retry
})
if (!normalizedQuery) {
@@ -672,8 +675,12 @@ export function AISearchWorkspace({
<span className="ai-search-kicker">✓ 已完成</span>
<h2>{resultQuery || query}</h2>
<p>
知识库已收录 {messageCount.toLocaleString()} 条消息 → 找到{' '}
{searchTrace?.retrievedEvidence || 0} 条相关消息 → {evidence.length} 条 Evidence →
知识库已收录 {messageCount.toLocaleString()} 条消息 →{' '}
{cachedAt
? `缓存中保留 ${evidenceCollection.length} 条 Evidence`
: searchTrace?.retrievedEvidence !== undefined
? `读取 ${searchTrace.retrievedEvidence} 条范围消息`
: `读取 ${evidence.length} 条消息`}{' '}→ {evidence.length} 条 Evidence →
已生成回答{cachedAt ? ' · 已使用缓存' : ''}
</p>
{searchTrace &&
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react'
import { aiSearchRangeStart } from '../../../../../shared/ai-search'
import { aiSearchRangeStart, inferAiSearchTimeRange } from '../../../../../shared/ai-search'
import type { AiSearchTimeRange } from '../../../../../shared/ai-search'
import {
RANGE_LABELS,
@@ -9,7 +9,6 @@ import {
buildSearchCacheKey,
parseSearchCacheKey,
readSearchCache,
readSearchCacheByQuery,
writeSearchCache
} from '../searchUtils'
import { createSearchCacheRecord, mapCacheRecordToResult } from '../searchMappers'
@@ -178,13 +177,15 @@ export function useSearchHistory({
setQuery(historyQuery)
setSelectedEvidence(0)
setHistoryOpen(false)
const resolvedTimeRange = inferAiSearchTimeRange(historyQuery, range, new Date())
const cacheKey = buildSearchCacheKey(
scope,
scope === 'conversation' ? conversationContactMd5 : '',
range,
historyQuery
historyQuery,
resolvedTimeRange
)
const cached = readSearchCache(cacheKey) || readSearchCacheByQuery(historyQuery)?.record || null
const cached = readSearchCache(cacheKey)
if (!cached) {
setAnswer('')
setEvidence([])
@@ -58,7 +58,10 @@ export const mapSearchResultToTrace = (
finalEvidenceCount: number
): SearchTrace => ({
knowledgeMessages: result.knowledge.indexedMessageCount,
retrievedEvidence: result.candidateEvidenceCount,
retrievedEvidence:
result.retrieval.intent === 'conversation_recall'
? result.retrieval.sourceMessageCount ?? result.candidateEvidenceCount
: result.candidateEvidenceCount,
finalEvidence: finalEvidenceCount,
timings: result.timings,
contextEvidence: result.contextEvidenceCount,
@@ -51,6 +51,30 @@ export const resolveSearchResultViewTransition = (
result: AiSearchPipelineResult,
range: SearchRange
): SearchResultViewTransition => {
if (result.status === 'understanding_failed') {
return {
stage: 'insufficient',
analysisError: result.error || '我没有完全理解你想怎么查,可以换一种说法。'
}
}
if (result.status === 'contact_not_found') {
return {
stage: 'insufficient',
analysisError: result.error || '我理解你在问这个联系人,但没有在当前通讯录中确认到对应联系人。'
}
}
if (result.status === 'ambiguous_contact') {
return {
stage: 'insufficient',
analysisError: result.error || '找到多个可能的联系人,暂时无法确定你指的是哪一个。'
}
}
if (result.status === 'no_messages') {
return {
stage: 'insufficient',
analysisError: result.error || '已经确认联系人,但当前可读取记录里没有对应聊天消息。'
}
}
if (result.status === 'no_evidence') {
return {
stage: 'insufficient',
@@ -1,5 +1,5 @@
import type { Contact, Message } from '../../../../shared/types'
import type { AiSearchTimeRange } from '../../../../shared/ai-search'
import { inferAiSearchTimeRange, type AiSearchTimeRange } from '../../../../shared/ai-search'
import type {
AISearchCacheRecord,
EvidenceItem,
@@ -315,8 +315,23 @@ export const buildSearchCacheKey = (
scope: SearchScope,
contactMd5: string,
range: SearchRange,
query: string
): string => JSON.stringify([scope, contactMd5, range, query.trim().toLowerCase()])
query: string,
resolvedTimeRange?: Pick<AiSearchTimeRange, 'startTime' | 'endTime'>,
knowledgeGeneration?: string
): string => {
const base = [scope, contactMd5, range, query.trim().toLowerCase()]
const hasResolvedTime = Boolean(
resolvedTimeRange &&
(resolvedTimeRange.startTime !== undefined || resolvedTimeRange.endTime !== undefined)
)
if (!hasResolvedTime && !knowledgeGeneration) return JSON.stringify(base)
return JSON.stringify([
...base,
hasResolvedTime ? resolvedTimeRange?.startTime ?? null : null,
hasResolvedTime ? resolvedTimeRange?.endTime ?? null : null,
knowledgeGeneration || null
])
}
export type CreateSearchRequestContextInput = {
query: string
@@ -324,6 +339,8 @@ export type CreateSearchRequestContextInput = {
range: SearchRange
timeRangeOverride?: AiSearchTimeRange
activeContactMd5?: string
now?: Date
knowledgeGeneration?: string
retry?: {
range: SearchRange
timeRangeOverride?: AiSearchTimeRange
@@ -335,6 +352,7 @@ export type SearchRequestContext = {
effectiveRange: SearchRange
effectiveTimeRangeOverride?: AiSearchTimeRange
conversationId?: string
resolvedTimeRange: AiSearchTimeRange
cacheKey: string
}
@@ -344,25 +362,41 @@ export const createSearchRequestContext = ({
range,
timeRangeOverride,
activeContactMd5,
now,
knowledgeGeneration,
retry
}: CreateSearchRequestContextInput): SearchRequestContext => {
const normalizedQuery = query.trim()
const effectiveRange = retry?.range || range
const effectiveTimeRangeOverride = retry?.timeRangeOverride || timeRangeOverride
const conversationId = scope === 'conversation' ? activeContactMd5 : undefined
const resolvedTimeRange = inferAiSearchTimeRange(
normalizedQuery,
effectiveRange,
now || new Date(),
effectiveTimeRangeOverride
)
return {
normalizedQuery,
effectiveRange,
effectiveTimeRangeOverride,
conversationId,
cacheKey: buildSearchCacheKey(scope, conversationId || '', effectiveRange, normalizedQuery)
resolvedTimeRange,
cacheKey: buildSearchCacheKey(
scope,
conversationId || '',
effectiveRange,
normalizedQuery,
resolvedTimeRange,
knowledgeGeneration
)
}
}
export const parseSearchCacheKey = (
key: string
): { scope: SearchScope; contactMd5: string; range: SearchRange; query: string } | null => {
): { scope: SearchScope; contactMd5: string; range: SearchRange; query: string; startTime?: number; endTime?: number } | null => {
try {
const parts = JSON.parse(key) as unknown
if (
@@ -377,7 +411,9 @@ export const parseSearchCacheKey = (
scope: parts[0] as SearchScope,
contactMd5: typeof parts[1] === 'string' ? parts[1] : '',
range: parts[2] as SearchRange,
query: parts[3]
query: parts[3],
startTime: typeof parts[4] === 'number' ? parts[4] : undefined,
endTime: typeof parts[5] === 'number' ? parts[5] : undefined
}
} catch {
return null