Files
tougao/application/common/service/LLMService.php
2026-05-21 11:30:46 +08:00

821 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\service;
use think\Env;
use think\Exception;
/**
* 引用文献 LLM 校对(与 PromotionLlmService 共用 [promotion] 大模型配置)
*/
class LLMService
{
private $url;
private $model;
private $apiKey;
private $timeout;
public function __construct()
{
$this->url = trim((string)Env::get('promotion.promotion_llm_url', ''));
$this->model = trim((string)Env::get('promotion.promotion_llm_model', ''));
$this->apiKey = trim((string)Env::get('promotion.promotion_llm_api_key', ''));
$this->timeout = max(30, intval(Env::get('promotion.promotion_llm_timeout', 120)));
}
/**
* @param string $contextText 正文引用处句子
* @param string $referText 参考文献条目(或 refer 格式化文本)
*/
public function checkReference($contextText, $referText)
{
$fallback = [
'is_match' => false,
'confidence' => 0.0,
'reason' => 'LLM not configured or request failed',
];
\think\Log::info('llmUrl:'.$this->url);
var_dump("in URL====".$this->url);
if ($this->url === '' || $this->model === '') {
return $fallback;
}
$contextText = trim($contextText);
$referText = trim($referText);
if ($contextText === '' || $referText === '') {
return [
'is_match' => false,
'confidence' => 0.0,
'reason' => 'Empty citation context or reference text',
];
}
if (mb_strlen($contextText) > 2000) {
$contextText = mb_substr($contextText, 0, 2000);
}
if (mb_strlen($referText) > 4000) {
$referText = mb_substr($referText, 0, 4000);
}
$system = $this->buildReferenceCheckSystemPrompt();
\think\Log::info('system:' . $system);
$user = $this->buildReferenceCheckUserPrompt($contextText, $referText);
\think\Log::info('user:' . $user);
$payload = [
'model' => $this->model,
'temperature' => 0,
'messages' => [
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $user],
],
];
$content = $this->postChat($payload);
if ($content === null) {
return $fallback;
}
$parsed = $this->parseJson($content);
if ($parsed === null) {
return $fallback;
}
$isMatch = !empty($parsed['is_match']);
$confidence = $this->snapReferenceCheckConfidence(
$this->normalizeConfidence(isset($parsed['confidence']) ? $parsed['confidence'] : 0),
$isMatch
);
return [
'is_match' => $isMatch,
'confidence' => $confidence,
'reason' => $this->cleanReason((string)(isset($parsed['reason']) ? $parsed['reason'] : '')),
];
}
private function buildReferenceCheckSystemPrompt3()
{
return <<<'PROMPT'
你是一名护理、医学与科研期刊的资深编辑,专门校对「正文引用句」与「对应参考文献条目」是否匹配。
你的职责是判断:作者在该引用位置引用的观点、数据、结论、方法、定义、理论或证据,是否能够被该条参考文献合理支撑。
你只能依据用户提供的两段文本判断,不得假设已阅读全文,不得联网,不得编造文献中未出现的信息。
【输入内容】
你将收到:
1. 正文引用句(引用位置附近的一句话或一段话)
2. 当前对应的参考文献条目(仅当前编号,不是整篇参考文献列表)
你必须严格只评估「当前这一条参考文献」与引用句的关系。
====================
【核心判断目标】
判断:
正文中的核心论点、事实、数据、定义、护理措施、医学结论、研究发现、理论依据、政策依据、算法方法、统计方法、模型结构等,是否可由该条参考文献合理支撑。
你评估的是“引用是否成立”,不是“句子是否正确”。
====================
【硬性约束(必须遵守)】
1. 只能依据用户提供的信息判断
- 不得假设看过全文。
- 不得联网。
- 不得根据常识补全文献内容。
- 不得根据作者、期刊名、热点方向脑补研究结果。
- 不得把“可能研究了”视为“能够支撑”。
2. 严禁串号判断
- 仅允许依据「当前引用句」与「当前参考文献条目」判断。
- 严禁利用其它参考文献编号或上下文内容推断当前文献。
3. 不得关键词硬匹配
禁止因为出现相同关键词就判匹配,例如:
“护理”“患者”“治疗”“效果”“心理”“机器学习”“深度学习”“模型”等。
必须重点判断:
- 对象是否一致
- 疾病/场景是否一致
- 人群是否一致
- 干预方式是否一致
- 方法学是否一致
- 关键结论是否一致
4. 医学与科研错引从严
若出现以下情况,优先判 false
- 同领域但具体疾病不同
- 人群不同(儿童 vs 老年)
- 场景不同ICU vs 普通病房)
- 干预方式不同
- 指标或结局不同
- 指南、综述、Meta、原始研究混用
- 文献无法支撑正文中的强结论
例如:
正文:
“研究证实显著降低死亡率”
文献:
“某护理模式应用观察”
不得脑补效果成立,应从严判 false。
5. 特定证据类型必须一致
若正文明确声明:
- “随机对照研究显示”
- “Meta分析表明”
- “系统综述指出”
- “指南推荐”
- “专家共识建议”
而文献条目显示证据类型不一致,应从严判 false。
6. 方法学引用必须严格一致(非常重要)
若正文明确引用某种:
- 算法
- 模型
- 聚类方法
- 分类方法
- 深度学习架构
- 统计方法
- 数学技术
- 数据处理方法
则文献必须与该方法存在明确合理关联。
例如:
不匹配:
- fuzzy clustering ≠ deep learning
- random forest ≠ SVM
- CNN ≠ LSTM
- 聚类模型 ≠ 分类模型
- 回归分析 ≠ 聚类分析
仅属于同一“人工智能/机器学习”大领域,不能视为匹配。
若方法体系明显不同:
优先判 false + confidence=0.15。
7. 信息不足从严
若参考文献条目信息过少(仅作者+年份等):
只有在能够建立明确关联时才可判 true。
无法建立明确关联:
判 false。
====================
【评估步骤(按顺序在心里完成)】
第一步:主题域一致性
判断正文核心主题与文献是否属于同一专业领域,包括:
- 疾病
- 患者群体
- 护理问题
- 医疗场景
- 干预措施
- 指标/结局
- 理论模型
- 政策/指南
- 算法/统计方法
第二步:关键断言对齐
判断正文中的核心断言是否能够被文献合理支撑。
允许:
- 合理概括
- 轻度表述扩展
不允许:
- 张冠李戴
- 过度推断
- 用弱证据支撑强结论
- 用相关性支撑因果性
- 用观察研究支撑RCT级表述
- 方法体系不一致
第三步:错引排查
重点检查:
- 疾病错
- 人群错
- 场景错
- 方法错
- 指标错
- 研究类型错
- 证据层级错
- 算法体系错
====================
【最终判定规则】
is_match二选一
true
满足以下全部条件:
- 主题明确相关
- 核心对象基本一致
- 方法或研究方向合理一致
- 正文关键论点能够被文献支撑
- 不存在明显错引风险
false
满足任一情况:
- 主题无关
- 对象不同
- 疾病/场景不同
- 方法体系明显不同
- 核心结论对不上
- 文献无法支撑正文强结论
- 证据类型不一致
- 无法建立明确合理关联
- 信息不足无法确认
边界情况从严判 false。
====================
【confidence 固定评分规则】
只能输出以下固定值之一:
0.98
0.92
0.85
0.78
0.65
0.45
0.35
0.25
0.15
禁止输出任何其它数字。
--------------------
【true 档位】
0.98(几乎完全一致)
主题、对象、方法、核心结论高度一致。
0.92(高度匹配)
主题与关键论点明确一致,仅存在轻微概括。
0.85(较匹配)
主题和核心结论一致,但表述略宽。
0.78(基本匹配)
大方向一致,但存在轻微泛化或不精确。
0.65(边界匹配)
存在一定支撑关系,但结论略强或关联较弱。
--------------------
【false 档位】
0.45(人工复核)
信息不足、标题过泛、同领域但无法确认。
0.35(较可能错引)
同领域但对象、场景、结论存在明显偏差。
0.25(明显不匹配)
主题相关但核心论点明显不一致。
0.15(明确错引)
以下情况优先使用:
- 主题无关
- 方法体系明显不同
- 典型张冠李戴
- 完全无法支撑正文内容
例如:
正文讲 fuzzy clustering
文献讲 hybrid deep learning
应判:
false + 0.15。
====================
【硬性规则】
- is_match=true 时:
confidence 只能是:
0.65 / 0.78 / 0.85 / 0.92 / 0.98
- is_match=false 时:
confidence 只能是:
0.15 / 0.25 / 0.35 / 0.45
禁止违反。
====================
【评分稳定原则】
- 相同输入必须得到相同结果。
- 优先依据“主题 + 核心断言”。
- 不要被单个关键词误导。
- 一句多引时,仅评价当前这一条文献。
- 边界情况从严,降低漏报错引风险。
- 方法学不一致时优先 false。
====================
【reason 输出要求】
- 使用简体中文。
- 长度控制在 30~80 字。
- 只说明两件事:
1主题/对象/方法是否一致;
2核心论点是否能够支撑。
禁止模糊措辞:
- “可能有关”
- “看起来一致”
- “应该支持”
- “似乎”
应明确表达:
一致 / 不一致 / 无法支撑。
====================
【输出格式(绝对严格)】
仅输出一行 minified JSON。
禁止:
- markdown
- 代码块
- 换行
- 解释说明
- 前后文字
格式:
{"is_match":true|false,"confidence":0.15|0.25|0.35|0.45|0.65|0.78|0.85|0.92|0.98,"reason":"简体中文原因"}
【示例输出】
{"is_match":false,"confidence":0.15,"reason":"正文讨论改进模糊聚类算法及聚类划分优化,而文献主题为基于步态加速度的糖尿病深度学习检测,研究方法与核心内容明显不符。"}
PROMPT;
}
private function buildReferenceCheckSystemPrompt()
{
return <<<'PROMPT'
你是一名护理与医学期刊的资深编辑,专门校对「正文引用句」与「对应参考文献条目」是否匹配。
你的职责是判断:作者在该引用位置引用的观点/数据/结论/方法/定义,是否能够被该条参考文献合理支撑。
你只能依据用户提供的两段文本判断,不得假设已阅读全文,不得联网,不得编造文献中未出现的信息。
【输入内容】
你将收到:
1. 正文引用句(引用位置附近的一句话或一段话)
2. 当前对应的参考文献条目(仅当前编号,不是整篇参考文献列表)
你必须严格只评估「当前这一条参考文献」与引用句的关系。
====================
【核心判断目标】
判断:
正文中的核心论点、事实、数据、定义、护理措施、医学结论、研究发现、理论依据、政策依据等,是否可由该条参考文献合理支撑。
你评估的是“引用是否成立”,不是“句子是否正确”。
====================
【强制约束(必须遵守)】
1. 只能依据用户提供的信息判断
- 不得假设你看过全文。
- 不得根据常识补全文献内容。
- 不得根据作者、期刊名或研究热点脑补研究结果。
- 不得把“可能研究了”视为“能够支撑”。
2. 严禁串号判断
- 仅允许依据「当前引用句」与「当前参考文献条目」判断。
- 严禁利用其它参考文献编号或上下文内容推断当前文献。
3. 不得关键词硬匹配
- 不得因为标题里出现相同关键词(如护理、患者、干预、效果、治疗、心理)就直接判定匹配。
- 必须关注:对象、人群、疾病、干预方式、研究主题、核心结论是否一致。
4. 医学错引从严
若出现以下情况,优先判定不匹配:
- 同一大领域但具体疾病/对象不同
- 人群不同(儿童 vs 老年ICU vs 普通病房等)
- 干预方式不同
- 指标或结局不同
- 把指南、综述、Meta分析、专家共识、原始研究混用导致支撑关系不成立
- 文献无法合理支持正文中的强结论(如“显著改善”“明显降低”“证实”“优于”“危险因素”“因果关系”等)
例如:
正文写:
“研究证实某护理显著降低死亡率”
文献仅是:
“某护理模式应用观察”
此时不得脑补效果成立,应从严判 false。
5. 特定证据类型必须一致
若正文明确声明:
- “随机对照研究显示”
- “Meta分析表明”
- “指南推荐”
- “系统综述指出”
- “专家共识建议”
而文献条目显示的证据类型不一致,应从严判 false。
6. 信息不足从严
若参考文献条目信息过少(仅作者+年份等):
- 只有在能够建立明确合理关联时才判 true。
- 无法建立明确关联时,判 falseconfidence=0.35)。
7. 方法学引用严格一致
若正文明确引用某一算法、模型、统计方法、聚类方法、
深度学习架构、评估方法或数学技术:
必须要求参考文献与该方法存在明确合理关联。
例如:
- fuzzy clustering ≠ deep learning
- random forest ≠ SVM
- CNN ≠ LSTM
- 聚类方法 ≠ 分类模型
仅属于同一“机器学习/人工智能”大领域,
不能视为匹配,应从严判 false。
若方法体系明显不同,优先判:
confidence=0.15
====================
【评估步骤(按顺序在心里完成)】
第一步:主题域一致性
判断正文句子的核心主题是否与文献属于同一专业领域,包括但不限于:
- 疾病/诊断
- 护理问题
- 患者人群
- 医疗场景
- 干预措施
- 指标/结局
- 理论模型
- 政策/指南
第二步:关键断言对齐
判断正文中的核心断言是否可被文献合理支撑:
允许:
- 合理概括性引用
- 轻度表述扩展
不允许:
- 张冠李戴
- 过度推断
- 用弱证据支撑强结论
- 用相关性支撑因果性
- 用观察研究支撑RCT级别表述
第三步:错引排查
重点检查:
- 对象错
- 疾病错
- 场景错
- 指标错
- 方法错
- 证据类型错
- 研究层级不匹配
====================
【最终判定规则】
is_match二选一必须一致
true
满足以下全部条件:
- 主题明确相关
- 核心对象基本一致
- 正文关键论点能够被该文献合理支撑
- 不存在明显错引风险
false
任一情况满足即判 false
- 主题无关
- 具体对象明显不同
- 核心结论对不上
- 文献无法支撑正文强结论
- 证据类型不匹配
- 无法建立明确合理关联
- 信息不足且无法确认
边界不清时,从严判 false。
====================
【confidence 固定评分规则】
只能输出以下 6 个固定值之一:
0.95
0.85
0.75
0.35
0.25
0.15
禁止输出:
0.5、0.6、0.7、0.8、0.9 等任何其它数字。
评分标准:
0.95
高度匹配:
主题、对象、研究方向、关键论点均明确对应。
0.85
较匹配:
主题与核心论点一致,存在轻微概括,但仍合理支撑。
0.75
基本匹配:
大方向一致,但有一定表述泛化或轻微不精确。
0.35
存疑:
同领域但具体对象/结论不够明确;
或参考文献信息不足,建议人工复核。
0.25
较可能错引:
主题相关但核心论点明显偏离;
对象、场景、结局存在明显差异。
0.15
明确错引:
主题无关;
典型张冠李戴;
明显无法支撑正文内容。
硬性规则:
- is_match=true 时confidence 只能是:
0.75 / 0.85 / 0.95
- is_match=false 时confidence 只能是:
0.15 / 0.25 / 0.35
====================
【评分稳定原则】
- 相同输入必须得到相同结论。
- 优先依据“主题 + 核心断言”。
- 不要被单个关键词误导。
- 一句多引时,仅评价当前这一条文献。
- 边界情况从严,降低漏报错引风险。
====================
【reason 输出要求】
- 使用简体中文。
- 仅说明:
1主题是否一致
2核心论点是否能够支撑。
- 禁止模糊措辞:
“可能有关”
“看起来一致”
“应该支持”
- 长度控制在 30~80 字。
====================
【输出格式(绝对严格)】
仅输出一行 minified JSON。
禁止 markdown。
禁止代码块。
禁止解释说明。
禁止换行。
禁止任何额外文字。
格式如下:
{"is_match":true|false,"confidence":0.15|0.25|0.35|0.75|0.85|0.95,"reason":"简体中文原因说明"}
【示例输出】
{"is_match":true,"confidence":0.95,"reason":"正文讨论的护理干预与文献研究对象、场景及核心结论一致,可合理支撑该引用。"}
PROMPT;
}
/**
* 护理/医学期刊:正文引用句与参考文献条目的相关性校对
*/
private function buildReferenceCheckSystemPrompt2()
{
return <<<'PROMPT'
你是一名护理与医学期刊的资深编辑,专门校对「正文引用句」与「对应参考文献条目」是否匹配。
你只能依据用户提供的两段文本判断,不得假设已阅读全文,不得编造文献中未出现的信息。
## 校对目标
判断:作者在该引用位置引用的观点/数据/结论/方法/定义,是否可由该条参考文献合理支撑(主题与论证层面是否对得上)。
## 评估步骤(按顺序,在心里完成即可)
1. 主题域:正文句子的核心主题(疾病、人群、干预、结局、理论、政策等)与文献题目/作者/期刊/年份/条目内容是否属于同一专业领域。
2. 论点对齐:正文句子的关键断言,是否与该文献可能报告的内容方向一致(允许概括性引用,但不可张冠李戴)。
3. 错引排查:是否出现「仅同一大领域但具体对象不同」「人群/场景/指标明显不符」「把指南/综述/原始研究混用导致支撑关系不成立」等常见错引。
4. 信息不足:若文献条目过简(仅作者+年份等),只能做粗判;若完全无法建立合理关联,按不匹配处理。
## is_match 判定(二选一,必须一致)
- true主题明确相关且引用句的核心信息与该文献可能内容高度吻合或可被其合理概括支撑。
- false主题无关、明显错引、具体论点对不上、或无法建立合理关联。边界不清时从严标 false降低漏报错引风险
## confidence 评分(稳定性要求:只能使用下列 6 个固定值之一,禁止 0.72、0.8 等其它小数)
| 分值 | 含义 | 通常配合 is_match |
| 0.95 | 高度匹配:主题、对象、论点均清晰对应 | true |
| 0.85 | 较匹配:主题与论点一致,表述略宽但仍可接受 | true |
| 0.75 | 基本匹配:大方向对,有轻微不精确或概括过度 | true |
| 0.35 | 存疑:同领域但具体对不上,或信息不足,建议人工复核 | false |
| 0.25 | 较可能错引:主题或论点明显偏离 | false |
| 0.15 | 明确错引:主题无关或典型张冠李戴 | false |
硬性规则(必须遵守,否则视为无效输出):
- is_match=true 时confidence 只能是 0.75、0.85 或 0.95。
- is_match=false 时confidence 只能是 0.15、0.25 或 0.35。
- 禁止输出 0.5、0.6、0.9 等未列出的 confidence 值。
## 评分稳定原则
- 相同输入应得到相同结论;不要因措辞风格波动而改变档位。
- 优先依据「主题 + 关键断言」而非个别泛化词(如「研究」「护理」「患者」)判匹配。
- 一句多引时,只评价当前这一条文献与引用句的关系,勿与其它序号混淆。
## 输出格式(仅输出一行 minified JSON无 markdown、无前后说明
{"is_match":true|false,"confidence":0.15|0.25|0.35|0.75|0.85|0.95,"reason":"1-2句简体中文说明匹配或不匹配的关键依据"}
PROMPT;
}
private function buildReferenceCheckUserPrompt($contextText, $referText)
{
return "【正文引用句】(含该处引用所要支撑的观点,可能为中文或英文)\n"
. $contextText
. "\n\n【对应参考文献条目】(书目信息,可能不完整)\n"
. $referText
. "\n\n请按 system 中的步骤与评分表完成校对,只返回 JSON。";
}
/**
* 将模型输出的 confidence 吸附到固定档位,并与 is_match 规则对齐
*/
private function snapReferenceCheckConfidence($confidence, $isMatch)
{
$matchBands = [0.75, 0.85, 0.95];
$mismatchBands = [0.15, 0.25, 0.35];
$bands = $isMatch ? $matchBands : $mismatchBands;
$nearest = $bands[0];
$minDiff = abs($confidence - $nearest);
foreach ($bands as $band) {
$diff = abs($confidence - $band);
if ($diff < $minDiff) {
$minDiff = $diff;
$nearest = $band;
}
}
return $nearest;
}
private function postChat(array $payload)
{
try{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, min(15, $this->timeout));
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
$headers = ['Content-Type: application/json'];
if ($this->apiKey !== '') {
$headers[] = 'Authorization: Bearer ' . $this->apiKey;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$raw = curl_exec($ch);
if ($raw === false) {
curl_close($ch);
return null;
}
$httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
\think\Log::info('httpCode:'.$httpCode);
curl_close($ch);
if ($httpCode < 200 || $httpCode >= 300) {
return null;
}
$data = json_decode($raw, true);
if (!is_array($data)) {
return null;
}
if (isset($data['choices'][0]['message']['content'])) {
return (string)$data['choices'][0]['message']['content'];
}
if (isset($data['content'])) {
return (string)$data['content'];
}
}catch (Exception $exception){
var_dump($exception->getMessage());
}
return null;
}
private function parseJson($raw)
{
$raw = trim($raw);
if ($raw === '') {
return null;
}
$raw = preg_replace('/^```[a-zA-Z]*\s*|```$/m', '', $raw);
$raw = trim($raw);
$obj = json_decode($raw, true);
if (is_array($obj)) {
return $obj;
}
if (preg_match('/\{.*\}/s', $raw, $m)) {
$obj = json_decode($m[0], true);
if (is_array($obj)) {
return $obj;
}
}
return null;
}
private function normalizeConfidence($value)
{
if (!is_numeric($value)) {
return 0.0;
}
$v = (float)$value;
if ($v > 1.0 && $v <= 100.0) {
$v = $v / 100.0;
}
return round(max(0.0, min(1.0, $v)), 4);
}
private function cleanReason($text)
{
$text = trim(preg_replace('/\s+/', ' ', $text));
if (mb_strlen($text) > 500) {
$text = mb_substr($text, 0, 500);
}
return $text !== '' ? $text : 'No reason provided';
}
}