AITermService.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. <?php
  2. namespace App\Services\AIAssistant;
  3. use App\DTO\Search\SearchDataDTO;
  4. use App\Http\Resources\AiModelResource;
  5. use App\Services\AIModelService;
  6. use App\Services\AuthService;
  7. use App\Services\OpenAIService;
  8. use App\Services\OpenSearchService;
  9. use App\Services\TermService;
  10. use Illuminate\Support\Facades\Log;
  11. class AITermService
  12. {
  13. protected $pageSize = 50;
  14. protected AiModelResource $model;
  15. protected $modelToken;
  16. protected ?bool $thinking = null;
  17. private $sysPrompt = <<<'md'
  18. 请根据提供的文献搜素结果,撰写一个巴利术语的简体中文百科词条。
  19. 搜素结果是json数组
  20. 字段
  21. - title:(标题)
  22. - content:(内容)
  23. - path:(章节路径)
  24. - link:(引用链接)
  25. link 是一个类似"{{para|id=202-1878|title=202-1878|style=reference}}" 的字符串,后面输出的时候请原样输出,不要做任何改变
  26. 要求:
  27. 1. 参考维基百科的形式和结构
  28. 2. 所有观点必须标明巴利文出处,使用我提供的link
  29. 3. 引用巴利文原文时使用引号并斜体
  30. 4. 提供完整的参考文献列表
  31. 5. 保持学术中立性和客观性
  32. 6. 请引用我提供的全部内容,不要有任何遗漏
  33. 7. 请在文档的开头输出一个模板 {{quality|pending}}。 这个模板后面不要有空行,直接跟后面的内容。
  34. **观点引用标准格式:**
  35. 《文献中文名》在《章节中文名》中指出/解释/说明:"巴利文原文"(中文翻译及必要说明)。[link]
  36. ### 引用标准与技术要求
  37. - **数据源绑定:** 请遍历我提供的 JSON 搜索结果。对于数组中的每一项,必须使用其对应的 `link` 字段值。
  38. - **硬性禁止:** 禁止在最终文档中出现 "link"、"引用链接" 或方括号占位符,必须替换为 JSON 中实际的链接文本。
  39. 如果某个观点有多个出处,请分别列出巴利文引用链接。范例
  40. 《文献中文名》在《章节中文名》中指出/解释/说明:"巴利文原文"(中文翻译及必要说明)。[link引用链接1][link引用链接2]
  41. 示例:
  42. 《疑惑度脱新注》在《染色学处注释》中指出:"*Kiriyākiriyanti nivāsanapārupanato, kappassa anādānato kiriyākiriyaṃ*"[9](穿着下衣、披上衣是作为,不采取如法措施是不作为,故为作为-不作为)。{{para|id=202-1878|title=202-1878|style=reference}}
  43. **引用处理规则(按优先级):**
  44. 1. 【有明确论断句】直接引用该句巴利文:
  45. 《X》在《Y》中指出:"巴利文原文"(译文)。[link]
  46. 2. 【叙事段落,无单一论断句】从段落中选取最能代表该段核心意思的
  47. 一个完整句子作为代表句引用,不得跳过巴利文:
  48. 《X》在《Y》中记载,[中文概述主要内容];原文云:"巴利文代表句"
  49. (该句译文)。(link)
  50. 3. 【段落过长】从原文中截取开头或核心句,以省略号表示省略:
  51. "巴利文开头...(省略)"(译文说明省略范围)。[link]
  52. **绝对禁止:** 任何观点陈述只有中文转述而没有对应巴利文引用。
  53. 如确实无法提取,须注明"(原文为纯叙事,节录如下)"并仍须给出
  54. 原文片段。
  55. **输出前自检:**
  56. 逐条检查每一个观点陈述,确认是否符合以下格式:
  57. [中文陈述] + "巴利文" + (译文) + [link]
  58. 若有不符合的条目,返回修改后再输出。
  59. 词条结构应包括:
  60. - 简短定义段落(不要标题直接输出段落内容)
  61. - 目录
  62. - 词源与定义
  63. - 其他的,文献中提及的内容分类
  64. - 参考文献
  65. - 相关条目
  66. - 分类标签
  67. 格式要求:
  68. - 使用Markdown格式
  69. - 标题层级清晰(#, ##, ###)
  70. - 直接输出百科正文,无需大标题
  71. - 引用格式:《文献中文名》在《章节中文名》中 + 动词 + "巴利文" + (巴利文的中文译文)[link]
  72. - 引用动词可用:指出、解释、说明、定义、描述、强调、阐述、论述等
  73. - 巴利文使用罗马转写
  74. - 关键术语首次出现时提供巴利文和中文对照
  75. 参考文献格式:
  76. [序号] 文献全称缩写, 具体章节, 标题, 段落编号
  77. 请在文档的底部输出分类标签
  78. 请根据以下分类体系,为给定的词条打上分类标签。
  79. **分类体系:**
  80. 一、经藏教义:1.1基本教义(苦、集、灭、道、缘起、十二缘起、三法印、无常、无我、五蕴、三宝、八正道、中道、善法、不善法)/1.2业与轮回(业、善业、不善业、无记业、业报、轮回、结生、再生、三界轮转)/1.3涅槃与解脱(涅槃、有余涅槃、无余涅槃、解脱、道、果、烦恼断除、结、漏)
  81. 二、阿毗达摩:2.1心路与心类(欲界心、色界心、无色界心、出世间心、善心、不善心、无记心、心路、五门心路、意门心路、速行、有分、结生心、死心、转向心)/2.2心所法(遍一切心心所、杂心所、不善心所、美心所、贪、嗔、痴、慢、邪见、掉举、信、念、慧、悲、喜、舍)/2.3色法(四大种、地界、水界、火界、风界、净色、所造色、色聚、业生色、心生色、时节生色、食生色、真实色、非真实色)/2.4缘起与发趣法(二十四缘、因缘、所缘缘、增上缘、俱生缘、亲依止缘、前生缘、后生缘、业缘、果报缘、根缘、禅缘、道缘、相应缘、不相应缘、有缘、无有缘)
  82. 三、禅修:3.1止禅(遍禅、不净观、随念、四梵住、入出息念、四界差别、禅相、取相、似相、近行定、安止定、禅那、禅支、无色定)/3.2观禅(名色分别、观智、生灭智、坏灭智、怖畏智、厌离智、行舍智、道智、果智、毘婆舍那、三相、无常随观、苦随观、无我随观、刹那定)
  83. 四、律学:4.1戒条罪类(波罗夷、僧残、不定、舍堕、单堕、悔过、众学、灭诤、比库戒、比库尼戒、学处、犯罪、无犯、违犯条件)/4.2僧团制度与羯磨(羯磨、白羯磨、白二羯磨、白四羯磨、结界、布萨、自恣、受具足、出家、僧团、四方僧、惩罚羯磨、和合)/4.3僧侣生活与器具(三衣、钵、住处、精舍、雨安居、迦提那衣、头陀行、乞食、日用器具、净食)
  84. 五、经典与文献:5.1经藏(长部、中部、相应部、增支部、小部、经名、品名、篇名)/5.2论藏与注疏(论藏、七论、义注、复注、清净道论、摄论、史书、藏外文献)/5.3本生与偈颂(本生、长老偈、长老尼偈、佛种姓、譬喻、天宫事、饿鬼事)
  85. 六、世界观:6.1三界与诸天(欲界、色界、无色界、欲界天、梵天界、净居天、人间、有情居、天界层次)/6.2地狱与恶趣(地狱、无间地狱、寒冰地狱、孤独地狱、饿鬼界、畜生界、阿修罗界、四恶趣)/6.3神灵与非人(天神、梵天、夜叉、龙族、乾达婆、非人、护法神)
  86. 七、人物:7.1佛(佛名、过去佛、二十八佛、独觉佛、菩萨)/7.2出家弟子(上首弟子、大弟子、比库弟子、比库尼弟子、沙弥、沙弥尼、在学尼)/7.3在家人(男居士、女居士、护法者、国王、婆罗门、施主、转轮圣王、王后、大长者)/7.4外道(外道名)
  87. 八、地理:8.1国家与城镇(十六大国、国家、村落、市镇、寺院名)/8.2水系(河流、湖泊、海洋)/8.3山岳(山名、山脉)
  88. 九、动植物:9.1动物(兽类、鸟类、爬行类、鱼类、昆虫、神话动物、龙族、金翅鸟、畜养动物、野生动物)/9.2植物(树木、花卉、草药、粮食作物、果实、圣树、菩提树类)
  89. 十、巴利语言与语法:10.1语法术语(名词、动词、形容词、副词、格、数、性、时态、语式、复合词类型、前缀、后缀)/10.2语法缩写与标注(词性标注、格标注、语态标注、使役态、引用标记、出处标注)
  90. **输出规则:**
  91. - 必须从以上分类体系中选取,不得自创分类
  92. - 一个词条可打多个标签,但通常不超过3个
  93. - 先输出一级分类,再输出二级分类,再输出具体标签
  94. - 格式严格为:{{category|一级分类}} {{category|二级分类}} {{category|标签}}
  95. - 一级分类去除编号,例如"人物"而非"七、人物"
  96. - 二级分类去除编号,例如"佛"而非"7.1佛"
  97. - 只输出标签,不输出任何解释
  98. ---
  99. 输出示例:
  100. # 分类标签
  101. {{category|经典与文献}} {{category|经藏}} {{category|中部}} {{category|义注}}
  102. md;
  103. private $sysPromptTags = <<<'md'
  104. 你是一个巴利语佛教百科词条分类专家。请根据以下分类体系,为给定的词条打上分类标签。
  105. 在分类标签的前面添加一个**词条概述**
  106. 分类标签
  107. 请根据以下分类体系,为给定的词条打上分类标签。
  108. **分类体系:**
  109. 一、经藏教义:1.1基本教义(苦、集、灭、道、缘起、十二缘起、三法印、无常、无我、五蕴、三宝、八正道、中道、善法、不善法)/1.2业与轮回(业、善业、不善业、无记业、业报、轮回、结生、再生、三界轮转)/1.3涅槃与解脱(涅槃、有余涅槃、无余涅槃、解脱、道、果、烦恼断除、结、漏)
  110. 二、阿毗达摩:2.1心路与心类(欲界心、色界心、无色界心、出世间心、善心、不善心、无记心、心路、五门心路、意门心路、速行、有分、结生心、死心、转向心)/2.2心所法(遍一切心心所、杂心所、不善心所、美心所、贪、嗔、痴、慢、邪见、掉举、信、念、慧、悲、喜、舍)/2.3色法(四大种、地界、水界、火界、风界、净色、所造色、色聚、业生色、心生色、时节生色、食生色、真实色、非真实色)/2.4缘起与发趣法(二十四缘、因缘、所缘缘、增上缘、俱生缘、亲依止缘、前生缘、后生缘、业缘、果报缘、根缘、禅缘、道缘、相应缘、不相应缘、有缘、无有缘)
  111. 三、禅修:3.1止禅(遍禅、不净观、随念、四梵住、慈、悲、喜、舍、入出息念、四界差别、禅相、取相、似相、近行定、安止定、禅那、禅支、无色定)/3.2观禅(名色分别、观智、生灭智、坏灭智、怖畏智、厌离智、行舍智、道智、果智、毘婆舍那、三相、无常随观、苦随观、无我随观、刹那定)
  112. 四、律学:4.1戒条罪类(波罗夷、僧残、不定、舍堕、单堕、悔过、众学、灭诤、比库戒、比库尼戒、学处、犯罪、无犯、违犯条件)/4.2僧团制度与羯磨(羯磨、白羯磨、白二羯磨、白四羯磨、结界、布萨、自恣、受具足、出家、僧团、四方僧、惩罚羯磨、和合)/4.3僧侣生活与器具(三衣、钵、住处、精舍、雨安居、迦提那衣、头陀行、乞食、日用器具、净食)
  113. 五、经典与文献:5.1经藏(长部、中部、相应部、增支部、小部、经名、品名、篇名)/5.2论藏与注疏(论藏、七论、义注、复注、清净道论、摄论、史书、藏外文献)/5.3本生与偈颂(本生、长老偈、长老尼偈、佛种姓、譬喻、天宫事、饿鬼事)
  114. 六、世界观:6.1三界与诸天(欲界、色界、无色界、欲界天、梵天界、净居天、人间、有情居、天界层次)/6.2地狱与恶趣(地狱、无间地狱、寒冰地狱、孤独地狱、饿鬼界、畜生界、阿修罗界、四恶趣)/6.3神灵与非人(天神、梵天、夜叉、龙族、乾达婆、非人、护法神)
  115. 七、人物:7.1佛(佛名、过去佛、二十八佛、独觉佛、菩萨)/7.2出家弟子(上首弟子、大弟子、比库弟子、比库尼弟子、沙弥、沙弥尼、在学尼)/7.3在家人(男居士、女居士、护法者、国王、婆罗门、施主、转轮圣王、王后、大长者)/7.4外道(外道名)
  116. 八、地理:8.1国家与城镇(十六大国、国家、村落、市镇、寺院名)/8.2水系(河流、湖泊、海洋)/8.3山岳(山名、山脉)
  117. 九、动植物:9.1动物(兽类、鸟类、爬行类、鱼类、昆虫、神话动物、龙族、金翅鸟、畜养动物、野生动物)/9.2植物(树木、花卉、草药、粮食作物、果实、圣树、菩提树类)
  118. 十、巴利语言与语法:10.1语法术语(名词、动词、形容词、副词、格、数、性、时态、语式、复合词类型、前缀、后缀)/10.2语法缩写与标注(词性标注、格标注、语态标注、使役态、引用标记、出处标注)
  119. **输出规则:**
  120. - 必须从以上分类体系中选取,不得自创分类
  121. - 一个词条可打多个标签,但通常不超过3个
  122. - 先输出一级分类,再输出二级分类,再输出具体标签
  123. - 格式严格为:`{{category|一级分类}}` `{{category|二级分类}}` `{{category|标签}}`
  124. - 一级分类去除编号,例如"人物"而非"七、人物"
  125. - 二级分类去除编号,例如"佛"而非"7.1佛"
  126. - 只输出标签,不输出任何解释
  127. ---
  128. 输出示例:
  129. (词条的概述)
  130. # 分类标签
  131. {{category|经典与文献}} {{category|经藏}} {{category|中部}} {{category|义注}}
  132. md;
  133. private $sysPromptMeaning = <<<'md'
  134. 你是一个巴利语佛教术语翻译专家。我会提供一个巴利术语,以及它的百科词条正文。
  135. 请你阅读百科词条正文,为这个巴利术语提炼出一个最恰当、简洁的简体中文释义。
  136. **输出规则:**
  137. - 只输出释义本身,不要任何解释、标点修饰、引号或模板
  138. - 释义应为简短的中文词语或短语,通常不超过 15 个字
  139. - 优先采用学界通用的标准译法
  140. - 不要重复输出巴利原词
  141. md;
  142. /**
  143. * Create a new command instance.
  144. *
  145. * @return void
  146. */
  147. public function __construct(
  148. protected AIModelService $modelService,
  149. protected OpenAIService $openAIService,
  150. protected TermService $termService
  151. ) {}
  152. public function setModel($id)
  153. {
  154. $this->model = $this->modelService->getModelById($id);
  155. $this->modelToken = AuthService::getUserToken($id);
  156. return $this;
  157. }
  158. /**
  159. * 设置 deepseek thinking 开关;null 表示不显式设置,沿用模型默认。
  160. */
  161. public function setThinking(?bool $thinking): static
  162. {
  163. $this->thinking = $thinking;
  164. return $this;
  165. }
  166. private function query(string $word): array
  167. {
  168. $search = app(OpenSearchService::class);
  169. // 组装搜索参数
  170. $params = [
  171. 'query' => $word,
  172. 'resourceType' => 'tipitaka',
  173. 'granularity' => 'paragraph',
  174. 'pageSize' => $this->pageSize,
  175. ];
  176. $result = $search->search($params);
  177. $dto = SearchDataDTO::fromArray($result);
  178. $res = [];
  179. foreach ($dto->hits->items as $key => $item) {
  180. $res[] = [
  181. 'title' => $item->title,
  182. 'content' => $item->content,
  183. 'path' => $item->path,
  184. 'pid' => $item->getParaId(),
  185. 'link' => $item->getParaLink(),
  186. ];
  187. }
  188. Log::debug('query '.count($res));
  189. return $res;
  190. }
  191. /**
  192. * 按 word 查找/新建社区词条(_community_translation_zh-hans_),并用 LLM 生成百科正文写入 note。
  193. *
  194. * @return string LLM 生成的百科词条正文
  195. */
  196. public function updateOrCreate(string $channelId, string $word): string
  197. {
  198. $term = $this->termService->findOrCreate($channelId, $word);
  199. return $this->update($term->guid);
  200. }
  201. public function update(string $id)
  202. {
  203. // 获取术语
  204. $term = $this->termService->getRaw($id);
  205. $content = $this->generateNote($term->word);
  206. $meaning = $this->generateMeaning($term->word, $content);
  207. $this->termService->update($id, ['note' => $content, 'meaning' => $meaning]);
  208. return $content;
  209. }
  210. /**
  211. * 全文搜索 word 命中段落,连同搜索结果发给 LLM 生成巴利术语百科正文,并输出引用自检日志。
  212. *
  213. * @return string LLM 生成的百科词条正文
  214. */
  215. private function generateNote(string $word): string
  216. {
  217. // 全文搜索
  218. $query = $this->query($word);
  219. $res = json_encode($query, JSON_UNESCAPED_UNICODE);
  220. $resText = "# 搜索结果\n```json\n{$res}\n```\n";
  221. $termText = "# 巴利术语\n\n{$word}\n\n";
  222. // LLM 生成
  223. $llm = $this->openAIService->setApiUrl($this->model['url'])
  224. ->setModel($this->model['model'])
  225. ->setApiKey($this->model['key'])
  226. ->setSystemPrompt($this->sysPrompt)
  227. ->setTemperature(0.5)
  228. ->setStream(false);
  229. if ($this->thinking !== null) {
  230. $llm = $llm->setThinking($this->thinking);
  231. }
  232. $response = $llm->send($resText.$termText);
  233. $content = $response['choices'][0]['message']['content'] ?? '';
  234. $content = str_replace("{{quality|pending}}\n", '{{quality|pending}}', $content);
  235. // 输出自检报告
  236. Log::debug('llm response', ['strlen' => $content]);
  237. $paraIds = $this->extractAllParaIds($content);
  238. Log::debug('has paragraph ref ', ['total' => count($paraIds), 'id' => $paraIds]);
  239. $searchPid = array_map(fn ($item) => $item['pid'], $query);
  240. $diff = array_values(array_diff($paraIds, $searchPid));
  241. Log::debug('diff', ['total' => count($diff), 'data' => $diff]);
  242. return $content;
  243. }
  244. /**
  245. * 根据已生成的百科词条正文,让 LLM 为巴利术语提炼一个简洁的简体中文释义。
  246. *
  247. * @return string LLM 提炼的简体中文释义;失败时回退为原词
  248. */
  249. private function generateMeaning(string $word, string $content): string
  250. {
  251. $termText = "# 巴利术语\n\n{$word}\n\n";
  252. $noteText = "# 百科词条正文\n\n{$content}\n";
  253. $llm = $this->openAIService->setApiUrl($this->model['url'])
  254. ->setModel($this->model['model'])
  255. ->setApiKey($this->model['key'])
  256. ->setSystemPrompt($this->sysPromptMeaning)
  257. ->setTemperature(0.3)
  258. ->setStream(false);
  259. if ($this->thinking !== null) {
  260. $llm = $llm->setThinking($this->thinking);
  261. }
  262. $response = $llm->send($termText.$noteText);
  263. $meaning = trim($response['choices'][0]['message']['content'] ?? '');
  264. Log::debug('llm meaning', ['word' => $word, 'meaning' => $meaning]);
  265. return $meaning !== '' ? $meaning : $word;
  266. }
  267. public function create(string $word) {}
  268. /**
  269. * Extract all unique ID values from MediaWiki template parameter strings
  270. *
  271. * Parses a string that may contain multiple "{{para|...}}" templates
  272. * and returns an array of unique 'id' parameter values found.
  273. *
  274. * @param string $str The input string containing zero or more {{para|...}} templates
  275. * @return array<int, string> Array of unique extracted ID values (e.g., ['16-1376', 'ABC-123'])
  276. * Returns empty array if no IDs are found
  277. *
  278. * @example
  279. * // Single template
  280. * extractAllParaIds('{{para|id=16-1376|title=test}}')
  281. * // returns ['16-1376']
  282. *
  283. * // Multiple templates with duplicates
  284. * extractAllParaIds('{{para|id=16-1376}} and {{para|id=16-1376|style=ref}}')
  285. * // returns ['16-1376'] (duplicate removed)
  286. *
  287. * // Multiple unique IDs
  288. * extractAllParaIds('{{para|id=16-1376}} {{para|id=ABC-123}} {{para|id=16-1376}}')
  289. * // returns ['16-1376', 'ABC-123']
  290. */
  291. public function extractAllParaIds(string $str): array
  292. {
  293. $ids = [];
  294. // Find all {{para|...}} patterns
  295. if (preg_match_all('/{{para\|(.*?)}}/', $str, $matches)) {
  296. foreach ($matches[1] as $content) {
  297. // Extract id= value from each template content
  298. if (preg_match('/id=([^|&}]+)/', $content, $idMatch)) {
  299. $ids[] = $idMatch[1];
  300. }
  301. }
  302. }
  303. // Remove duplicates and preserve order of first occurrence
  304. return array_values(array_unique($ids));
  305. }
  306. }