参考文献校对失败重新校对
This commit is contained in:
@@ -1503,6 +1503,29 @@ class References extends Base
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅重跑某篇文章下 status=3(校对失败)的相关性记录(不清空,不抓摘要,不清洗文献内容)
|
||||||
|
* 联合引用组会整组重跑。
|
||||||
|
* POST: p_article_id
|
||||||
|
*/
|
||||||
|
public function referenceRelevanceCheckRecheckFailedAI()
|
||||||
|
{
|
||||||
|
$aParam = $this->request->post();
|
||||||
|
if (empty($aParam)) {
|
||||||
|
$aParam = $this->request->param();
|
||||||
|
}
|
||||||
|
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
|
||||||
|
if ($iPArticleId <= 0) {
|
||||||
|
return jsonError('p_article_id is required');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$result = (new ReferenceRelevanceCheckService())->recheckFailedOnlyByArticle($iPArticleId);
|
||||||
|
return jsonSuccess($result);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return jsonError($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated 支撑力度校对已下线,请使用 allReferenceCheckAI(主题相关性校对)
|
* @deprecated 支撑力度校对已下线,请使用 allReferenceCheckAI(主题相关性校对)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -204,6 +204,77 @@ class ReferenceRelevanceCheckService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅重新校对某篇文章下 status=3(失败)的记录;不刷新 refer_text / 文献摘要,只重置结果字段后入队。
|
||||||
|
* 联合引用组会整组重跑(与 enqueueRecheckFailedByPReferIdWithGroup 一致)。
|
||||||
|
*
|
||||||
|
* @param int $pArticleId
|
||||||
|
* @return array{p_article_id:int,reset:int,queued:int,check_ids:int[],transport:string,queue:string}
|
||||||
|
*/
|
||||||
|
public function recheckFailedOnlyByArticle($pArticleId)
|
||||||
|
{
|
||||||
|
$pArticleId = intval($pArticleId);
|
||||||
|
if ($pArticleId <= 0) {
|
||||||
|
throw new \InvalidArgumentException('p_article_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
DbReconnectHelper::ensure();
|
||||||
|
$rows = Db::name('article_reference_relevance_check_result')
|
||||||
|
->where('p_article_id', $pArticleId)
|
||||||
|
->where('status', self::RECORD_FAILED)
|
||||||
|
->order('reference_no asc,am_id asc,text_start asc,id asc')
|
||||||
|
->select();
|
||||||
|
if (empty($rows)) {
|
||||||
|
return [
|
||||||
|
'p_article_id' => $pArticleId,
|
||||||
|
'reset' => 0,
|
||||||
|
'queued' => 0,
|
||||||
|
'check_ids' => [],
|
||||||
|
'transport' => self::TRANSPORT_RABBITMQ,
|
||||||
|
'queue' => self::TRANSPORT_RABBITMQ,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$resetFields = $this->relevanceCheckResultResetFields([
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$targetRows = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$groupRows = $this->findCitationGroupRows($row);
|
||||||
|
foreach ($groupRows as $gr) {
|
||||||
|
$checkId = $this->resolveCheckRowId($gr);
|
||||||
|
if ($checkId > 0) {
|
||||||
|
$targetRows[$checkId] = $gr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pendingJobs = [];
|
||||||
|
foreach ($targetRows as $row) {
|
||||||
|
$checkId = $this->resolveCheckRowId($row);
|
||||||
|
Db::name('article_reference_relevance_check_result')->where('id', $checkId)->update($resetFields);
|
||||||
|
$pendingJobs[] = [
|
||||||
|
'check_id' => $checkId,
|
||||||
|
'reference_no' => intval($row['reference_no']),
|
||||||
|
'am_id' => intval($row['am_id']),
|
||||||
|
'text_start' => intval($row['text_start']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkIds = $this->enqueueChecksSortedByReferenceNo($pendingJobs, $pArticleId, 'recheck_failed');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'p_article_id' => $pArticleId,
|
||||||
|
'reset' => count($targetRows),
|
||||||
|
'queued' => count($checkIds),
|
||||||
|
'check_ids' => $checkIds,
|
||||||
|
'transport' => self::TRANSPORT_RABBITMQ,
|
||||||
|
'queue' => self::TRANSPORT_RABBITMQ,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 某条参考文献下「校对失败」的明细重新校对(异步)
|
* 某条参考文献下「校对失败」的明细重新校对(异步)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -25,13 +25,15 @@ class ReferenceRelevanceLlmService
|
|||||||
$this->url = trim((string)Env::get('promotion.promotion_llm_url', ''));
|
$this->url = trim((string)Env::get('promotion.promotion_llm_url', ''));
|
||||||
$this->model = trim((string)Env::get('promotion.promotion_llm_model', ''));
|
$this->model = trim((string)Env::get('promotion.promotion_llm_model', ''));
|
||||||
$this->apiKey = trim((string)Env::get('promotion.promotion_llm_api_key', ''));
|
$this->apiKey = trim((string)Env::get('promotion.promotion_llm_api_key', ''));
|
||||||
$this->timeout = max(180, intval(Env::get('promotion.promotion_llm_timeout', 180)));
|
// 相关性校对固定至少 200s(不跟 promotion_llm_timeout=120);可用 relevance_llm_timeout 单独加大
|
||||||
|
$timeout = intval(Env::get('promotion.relevance_llm_timeout', 0));
|
||||||
|
$this->timeout = max(200, $timeout > 0 ? $timeout : 200);
|
||||||
// 控制发送给 LLM 的上下文长度,降低单次推理耗时(可通过 env 覆盖)
|
// 控制发送给 LLM 的上下文长度,降低单次推理耗时(可通过 env 覆盖)
|
||||||
$this->maxSectionChars = max(1500, intval(Env::get('promotion.relevance_llm_max_section_chars', 4500)));
|
$this->maxSectionChars = max(1500, intval(Env::get('promotion.relevance_llm_max_section_chars', 4500)));
|
||||||
$this->maxLocalContextChars = max(600, intval(Env::get('promotion.relevance_llm_max_local_context_chars', 1800)));
|
$this->maxLocalContextChars = max(600, intval(Env::get('promotion.relevance_llm_max_local_context_chars', 1800)));
|
||||||
$this->maxReferChars = max(1500, intval(Env::get('promotion.relevance_llm_max_refer_chars', 3500)));
|
$this->maxReferChars = max(1500, intval(Env::get('promotion.relevance_llm_max_refer_chars', 3500)));
|
||||||
$this->maxAbstractChars = max(1500, intval(Env::get('promotion.relevance_llm_max_abstract_chars', 3500)));
|
$this->maxAbstractChars = max(1500, intval(Env::get('promotion.relevance_llm_max_abstract_chars', 3500)));
|
||||||
$this->maxTokens = max(4096, intval(Env::get('promotion.relevance_llm_max_tokens', 0)));
|
$this->maxTokens = max(0, intval(Env::get('promotion.relevance_llm_max_tokens', 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,6 +72,47 @@ class ReferenceRelevanceLlmService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$refCount = $this->countCiteGroupRefs($citeGroupRefs);
|
$refCount = $this->countCiteGroupRefs($citeGroupRefs);
|
||||||
|
// 默认每批最多 4 篇,降低单次排队/超时风险(可用 env 覆盖)
|
||||||
|
$maxRefsPerCall = max(2, intval(Env::get('promotion.relevance_llm_max_refs_per_call', 4)));
|
||||||
|
if ($refCount > $maxRefsPerCall) {
|
||||||
|
return $this->checkRelevanceByChunks(
|
||||||
|
$sectionText,
|
||||||
|
$localContext,
|
||||||
|
$referText,
|
||||||
|
$abstractText,
|
||||||
|
$citeGroupRefs,
|
||||||
|
$referTypeMap,
|
||||||
|
$refCount,
|
||||||
|
$maxRefsPerCall
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->checkRelevanceOnce(
|
||||||
|
$sectionText,
|
||||||
|
$localContext,
|
||||||
|
$referText,
|
||||||
|
$abstractText,
|
||||||
|
$citeGroupRefs,
|
||||||
|
$referTypeMap,
|
||||||
|
$refCount,
|
||||||
|
$fallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{results:array,request_failed?:bool,reason?:string} $fallback
|
||||||
|
* @return array{results:array,claims?:array,combined_relevance_score?:float,combined_reason?:string,request_failed?:bool,reason?:string}
|
||||||
|
*/
|
||||||
|
private function checkRelevanceOnce(
|
||||||
|
$sectionText,
|
||||||
|
$localContext,
|
||||||
|
$referText,
|
||||||
|
$abstractText,
|
||||||
|
$citeGroupRefs,
|
||||||
|
array $referTypeMap,
|
||||||
|
$refCount,
|
||||||
|
array $fallback
|
||||||
|
) {
|
||||||
$payload = [
|
$payload = [
|
||||||
'model' => $this->model,
|
'model' => $this->model,
|
||||||
'temperature' => 0,
|
'temperature' => 0,
|
||||||
@@ -135,6 +178,28 @@ class ReferenceRelevanceLlmService
|
|||||||
return array_merge($fallback, ['reason' => 'LLM returned empty or invalid results' . $detail]);
|
return array_merge($fallback, ['reason' => 'LLM returned empty or invalid results' . $detail]);
|
||||||
}
|
}
|
||||||
if ($refCount > 1 && count($results) < $refCount) {
|
if ($refCount > 1 && count($results) < $refCount) {
|
||||||
|
if ($refCount >= 4) {
|
||||||
|
$chunkSize = max(3, intval(ceil($refCount / 2)));
|
||||||
|
$retry = $this->checkRelevanceByChunks(
|
||||||
|
$sectionText,
|
||||||
|
$localContext,
|
||||||
|
$referText,
|
||||||
|
$abstractText,
|
||||||
|
$citeGroupRefs,
|
||||||
|
$referTypeMap,
|
||||||
|
$refCount,
|
||||||
|
$chunkSize
|
||||||
|
);
|
||||||
|
if (empty($retry['request_failed']) && count($retry['results']) >= $refCount) {
|
||||||
|
\think\Log::warning(sprintf(
|
||||||
|
'ReferenceRelevanceLlm: recovered via split retry (%d refs, chunk=%d) cite_group_refs=%s',
|
||||||
|
$refCount,
|
||||||
|
$chunkSize,
|
||||||
|
$citeGroupRefs
|
||||||
|
));
|
||||||
|
return $retry;
|
||||||
|
}
|
||||||
|
}
|
||||||
$saved = $this->saveBadJsonResponse($content, [
|
$saved = $this->saveBadJsonResponse($content, [
|
||||||
'cite_group_refs' => $citeGroupRefs,
|
'cite_group_refs' => $citeGroupRefs,
|
||||||
'section_chars' => mb_strlen($sectionText),
|
'section_chars' => mb_strlen($sectionText),
|
||||||
@@ -539,20 +604,20 @@ PROMPT;
|
|||||||
private function resolveMaxTokens($refCount)
|
private function resolveMaxTokens($refCount)
|
||||||
{
|
{
|
||||||
$refCount = max(1, intval($refCount));
|
$refCount = max(1, intval($refCount));
|
||||||
// reason/combined_reason 加详后,单篇输出更长,相应上调 token 预算防截断
|
// reason 已压缩;过高 max_tokens 会拖慢本地推理排队,按条数给够即可
|
||||||
$dynamic = min(16384, max(6144, $refCount * 1600));
|
$dynamic = min(8192, max(3072, $refCount * 900 + 1200));
|
||||||
if ($refCount >= 6) {
|
if ($refCount >= 4) {
|
||||||
$dynamic = max($dynamic, 12288);
|
$dynamic = max($dynamic, 5120);
|
||||||
}
|
}
|
||||||
if ($this->maxTokens > 0) {
|
if ($this->maxTokens > 0) {
|
||||||
return max($this->maxTokens, $dynamic);
|
return $this->maxTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $dynamic;
|
return $dynamic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 响应截断导致缺条时,用同组已有 combined_* 与中位单条分补全(最多补 2 条)。
|
* 响应截断导致缺条时,用同组已有 combined_* 与中位单条分补全(非一致分时最多补 2 条;一致分时可补全)。
|
||||||
*/
|
*/
|
||||||
private function fillMissingGroupResults(array $out, $citeGroupRefs, $refCount, $combinedScore = 0, $combinedReason = '')
|
private function fillMissingGroupResults(array $out, $citeGroupRefs, $refCount, $combinedScore = 0, $combinedReason = '')
|
||||||
{
|
{
|
||||||
@@ -575,7 +640,11 @@ PROMPT;
|
|||||||
$missing[] = $refNo;
|
$missing[] = $refNo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (empty($missing) || count($missing) > 2 || empty($out)) {
|
if (empty($missing) || empty($out)) {
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
$uniformScore = $this->resultsHaveUniformScore($out);
|
||||||
|
if (!$uniformScore && count($missing) > 2) {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,6 +673,182 @@ PROMPT;
|
|||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 大引用组拆批调用 LLM,合并逐篇结果后再计算联合分。
|
||||||
|
*
|
||||||
|
* @return array{results:array,claims?:array,combined_relevance_score?:float,combined_reason?:string,request_failed?:bool,reason?:string}
|
||||||
|
*/
|
||||||
|
private function checkRelevanceByChunks(
|
||||||
|
$sectionText,
|
||||||
|
$localContext,
|
||||||
|
$referText,
|
||||||
|
$abstractText,
|
||||||
|
$citeGroupRefs,
|
||||||
|
array $referTypeMap,
|
||||||
|
$refCount,
|
||||||
|
$chunkSize
|
||||||
|
) {
|
||||||
|
$fallback = [
|
||||||
|
'results' => [],
|
||||||
|
'request_failed' => true,
|
||||||
|
'reason' => 'LLM split batch failed',
|
||||||
|
];
|
||||||
|
$refNums = $this->parseCiteGroupRefNumbers($citeGroupRefs);
|
||||||
|
if (empty($refNums)) {
|
||||||
|
return array_merge($fallback, ['reason' => 'Empty cite_group_refs']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunkSize = max(2, intval($chunkSize));
|
||||||
|
$chunks = array_chunk($refNums, $chunkSize);
|
||||||
|
$allResults = [];
|
||||||
|
$claims = [];
|
||||||
|
$combinedScore = 0.0;
|
||||||
|
$combinedReason = '';
|
||||||
|
|
||||||
|
foreach ($chunks as $chunk) {
|
||||||
|
$chunkRefs = implode(',', $chunk);
|
||||||
|
$chunkRefer = $this->filterRefBlocks($referText, $chunk);
|
||||||
|
if ($chunkRefer === '') {
|
||||||
|
$chunkRefer = $referText;
|
||||||
|
}
|
||||||
|
$chunkAbstract = $this->filterRefBlocks($abstractText, $chunk);
|
||||||
|
$chunkTypeMap = [];
|
||||||
|
foreach ($chunk as $refNo) {
|
||||||
|
if (isset($referTypeMap[$refNo])) {
|
||||||
|
$chunkTypeMap[$refNo] = $referTypeMap[$refNo];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$part = $this->checkRelevanceOnce(
|
||||||
|
$sectionText,
|
||||||
|
$localContext,
|
||||||
|
$chunkRefer,
|
||||||
|
$chunkAbstract,
|
||||||
|
$chunkRefs,
|
||||||
|
$chunkTypeMap,
|
||||||
|
count($chunk),
|
||||||
|
$fallback
|
||||||
|
);
|
||||||
|
if (!empty($part['request_failed']) || empty($part['results'])) {
|
||||||
|
$reason = isset($part['reason']) ? (string)$part['reason'] : 'LLM split batch failed';
|
||||||
|
return array_merge($fallback, ['reason' => $reason]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($claims) && !empty($part['claims']) && is_array($part['claims'])) {
|
||||||
|
$claims = $part['claims'];
|
||||||
|
}
|
||||||
|
if ($combinedScore <= 0 && isset($part['combined_relevance_score'])) {
|
||||||
|
$combinedScore = floatval($part['combined_relevance_score']);
|
||||||
|
$combinedReason = (string)(isset($part['combined_reason']) ? $part['combined_reason'] : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($part['results'] as $row) {
|
||||||
|
$refNo = intval(isset($row['reference_no']) ? $row['reference_no'] : 0);
|
||||||
|
if ($refNo > 0 && !isset($allResults[$refNo])) {
|
||||||
|
$allResults[$refNo] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($allResults, SORT_NUMERIC);
|
||||||
|
$results = array_values($allResults);
|
||||||
|
if (count($results) < count($refNums)) {
|
||||||
|
return array_merge($fallback, [
|
||||||
|
'reason' => sprintf(
|
||||||
|
'LLM split batch incomplete: got %d/%d for cite_group_refs=%s',
|
||||||
|
count($results),
|
||||||
|
count($refNums),
|
||||||
|
$citeGroupRefs
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($results) > 1) {
|
||||||
|
$bands = $this->getScoreBands();
|
||||||
|
$adjustedCombined = $this->enforceCombinedAgainstSingles($results, $combinedScore, $bands);
|
||||||
|
if ($combinedScore <= 0 || abs($adjustedCombined - $combinedScore) > 0.001) {
|
||||||
|
$combinedScore = $adjustedCombined;
|
||||||
|
$combinedReason = $this->fallbackReasonFromScore(
|
||||||
|
$combinedScore,
|
||||||
|
$this->levelFromScore($combinedScore)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} elseif (count($results) === 1) {
|
||||||
|
$combinedScore = floatval($results[0]['relevance_score'] ?? 0);
|
||||||
|
$combinedReason = (string)($results[0]['reason'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
\think\Log::info(sprintf(
|
||||||
|
'ReferenceRelevanceLlm: split %d refs into %d batches (chunk=%d) cite_group_refs=%s',
|
||||||
|
$refCount,
|
||||||
|
count($chunks),
|
||||||
|
$chunkSize,
|
||||||
|
$citeGroupRefs
|
||||||
|
));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'results' => $results,
|
||||||
|
'claims' => $claims,
|
||||||
|
'combined_relevance_score' => $combinedScore,
|
||||||
|
'combined_reason' => $combinedReason,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按编号过滤「【参考文献 N】」块。
|
||||||
|
*/
|
||||||
|
private function filterRefBlocks($text, array $refNos)
|
||||||
|
{
|
||||||
|
$text = trim((string)$text);
|
||||||
|
if ($text === '' || empty($refNos)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$want = [];
|
||||||
|
foreach ($refNos as $refNo) {
|
||||||
|
$want[intval($refNo)] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocks = preg_split('/\n(?=【参考文献 \d+】)/u', $text);
|
||||||
|
$out = [];
|
||||||
|
$preamble = '';
|
||||||
|
foreach ($blocks as $block) {
|
||||||
|
$block = trim($block);
|
||||||
|
if ($block === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preg_match('/^【参考文献 (\d+)】/u', $block, $m)) {
|
||||||
|
$refNo = intval($m[1]);
|
||||||
|
if (!empty($want[$refNo])) {
|
||||||
|
$out[] = $block;
|
||||||
|
}
|
||||||
|
} elseif ($preamble === '') {
|
||||||
|
$preamble = $block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($preamble !== '' && !empty($out)) {
|
||||||
|
array_unshift($out, $preamble);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n\n", $out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resultsHaveUniformScore(array $rows)
|
||||||
|
{
|
||||||
|
$scores = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (!isset($row['relevance_score'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$scores[] = round(floatval($row['relevance_score']), 2);
|
||||||
|
}
|
||||||
|
if (count($scores) <= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count(array_unique($scores)) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
private function parseCiteGroupRefNumbers($citeGroupRefs)
|
private function parseCiteGroupRefNumbers($citeGroupRefs)
|
||||||
{
|
{
|
||||||
$citeGroupRefs = trim((string)$citeGroupRefs);
|
$citeGroupRefs = trim((string)$citeGroupRefs);
|
||||||
|
|||||||
Reference in New Issue
Block a user