llm

news/2026/1/19 16:11:08/文章来源:https://www.cnblogs.com/bigdata6666/p/19502237
import logging
import json
import difflib
import re
import os
import requests
import pytesseract

from PIL import Image, ImageOps
from io import BytesIO
from typing import Union, List, Dict, Optional, Any, Tuple
from tenacity import retry, stop_after_attempt, wait_random
from openai import OpenAI # 只需要 OpenAI 客户端

from label_studio_ml.model import LabelStudioMLBase
from label_studio_ml.response import ModelResponse
from label_studio_sdk.label_interface.objects import PredictionValue
from label_studio_sdk.label_interface.object_tags import ImageTag, ParagraphsTag
from label_studio_sdk.label_interface.control_tags import ControlTag, ObjectTag

logger = logging.getLogger(__name__)

# =======================
# SiliconFlow 固定配置
# =======================
SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1"
SILICONFLOW_API_KEY = "sk-deezdpffwyyhwglpqbyfyskukllsqjwbhutfgjwjwxnjdzvf" # TODO: 换成你的真实密钥
SILICONFLOW_MODEL = "Qwen/Qwen3-8B"

# 固定的系统提示词(角色设定)
SYSTEM_PROMPT = "你是一个擅长中文文本摘要的助手。请在保留关键信息的前提下用简洁的中文进行概括。"

# 固定的摘要任务指令
TASK_INSTRUCTION = "请对下面的文本进行摘要,突出主要内容,长度控制在200字以内。"


@retry(wait=wait_random(min=5, max=10), stop=stop_after_attempt(6))
def chat_completion_call(messages, params, *args, **kwargs):
"""
使用 openai SDK 以 SiliconFlow 为后端,调用 chat completions
"""
# 忽略 params 里的 provider / api_key / model,统一使用写死的 SiliconFlow 配置
client = OpenAI(
base_url=SILICONFLOW_BASE_URL,
api_key=SILICONFLOW_API_KEY,
)

temperature = params.get("temperature", OpenAIInteractive.TEMPERATURE)
n = params.get("num_responses", OpenAIInteractive.NUM_RESPONSES)

request_params = {
"messages": messages,
"model": SILICONFLOW_MODEL,
"n": n,
"temperature": temperature,
"max_tokens": 1000, # 如有需要可调整
}

logger.info(f"SiliconFlow(OpenAI SDK) request_params: {request_params}")
completion = client.chat.completions.create(**request_params)
logger.info(f"SiliconFlow(OpenAI SDK) completion: {completion}")

return completion


def gpt(messages: Union[List[Dict], str], params, *args, **kwargs):
"""
使用固定的系统提示 + 摘要任务指令,调用 SiliconFlow Qwen 模型,返回 List[str]
"""
full_messages: List[Dict[str, str]] = []

# System 角色:设定为中文摘要助手
full_messages.append({"role": "system", "content": SYSTEM_PROMPT})

# 如果传入是字符串,把它当成“要摘要的原文”
if isinstance(messages, str):
user_content = f"{TASK_INSTRUCTION}\n\n原文如下:\n{messages}"
full_messages.append({"role": "user", "content": user_content})
else:
# 兼容原来的调用方式:messages 为 list[dict]
# 在第一个 user 消息前插入 TASK_INSTRUCTION
inserted = False
for m in messages:
if m.get("role") == "user" and not inserted:
new_content = f"{TASK_INSTRUCTION}\n\n原文如下:\n{m.get('content', '')}"
full_messages.append({"role": "user", "content": new_content})
inserted = True
else:
full_messages.append(m)
if not inserted:
# 如果没有 user 消息则补一个
full_messages.append({"role": "user", "content": TASK_INSTRUCTION})

logger.info(f"SiliconFlow(OpenAI SDK) request messages: {full_messages}, params={params}")
completion = chat_completion_call(full_messages, params)
logger.info(f"SiliconFlow(OpenAI SDK) response: {completion}")

# completion.choices[i].message.content
response = [choice.message.content for choice in completion.choices]
return response


class OpenAIInteractive(LabelStudioMLBase):
"""
基于 SiliconFlow Qwen 模型的 Text Summarization ML backend
使用 Label Studio 的 OpenAIInteractive 模板,底层改为 SiliconFlow
"""

# 这些环境变量可以保留(部分仍被使用)
OPENAI_PROVIDER = os.getenv("OPENAI_PROVIDER", "openai")
OPENAI_KEY = os.getenv('OPENAI_API_KEY')
PROMPT_PREFIX = os.getenv("PROMPT_PREFIX", "prompt")
USE_INTERNAL_PROMPT_TEMPLATE = bool(int(os.getenv("USE_INTERNAL_PROMPT_TEMPLATE", 1)))
# 可选:会被 setup() 当作文件路径读取;但摘要逻辑主要由 SYSTEM_PROMPT/TASK_INSTRUCTION 控制
DEFAULT_PROMPT = os.getenv('DEFAULT_PROMPT')
PROMPT_TEMPLATE = os.getenv(
"PROMPT_TEMPLATE",
'**Source Text**:\n\n"{text}"\n\n**Task Directive**:\n\n"{prompt}"'
)
PROMPT_TAG = "TextArea"
SUPPORTED_INPUTS = ("Image", "Text", "HyperText", "Paragraphs")
NUM_RESPONSES = int(os.getenv("NUM_RESPONSES", 1))
TEMPERATURE = float(os.getenv("TEMPERATURE", 0.7))
OPENAI_MODEL = os.getenv("OPENAI_MODEL")
AZURE_RESOURCE_ENDPOINT = os.getenv("AZURE_RESOURCE_ENDPOINT", '')
AZURE_DEPLOYMENT_NAME = os.getenv("AZURE_DEPLOYMENT_NAME")
AZURE_API_VERSION = os.getenv("AZURE_API_VERSION", "2023-05-15")
OLLAMA_ENDPOINT = os.getenv("OLLAMA_ENDPOINT")

def setup(self):
if self.DEFAULT_PROMPT and os.path.isfile(self.DEFAULT_PROMPT):
logger.info(f"Reading default prompt from file: {self.DEFAULT_PROMPT}")
with open(self.DEFAULT_PROMPT, encoding="utf-8") as f:
self.DEFAULT_PROMPT = f.read()

def _ocr(self, image_url):
# Open the image containing the text
response = requests.get(image_url)
image = Image.open(BytesIO(response.content))
image = ImageOps.exif_transpose(image)

# Run OCR on the image
text = pytesseract.image_to_string(image)
return text

def _get_text(self, task_data, object_tag):
"""
根据 object_tag 类型,从 task_data 中提取文本:
- Image: OCR
- Paragraphs: JSON 序列化
- Text/HyperText: 原样返回
"""
data = task_data.get(object_tag.value_name)

if data is None:
return None

if isinstance(object_tag, ImageTag):
return self._ocr(data)
elif isinstance(object_tag, ParagraphsTag):
return json.dumps(data, ensure_ascii=False)
else:
return data

def _get_prompts(self, context, prompt_tag) -> List[str]:
"""获取 Prompt 的值(交互模式 / 存储 / 默认 prompt)"""
if context:
# 交互模式 - 从 context 里读当前 prompt
result = context.get('result')
if result:
for item in result:
if item.get('from_name') == prompt_tag.name:
return item['value']['text']
# 初始化 - 从内部存储读取
elif prompt := self.get(prompt_tag.name):
return [prompt]
# 默认 prompt(注意,如果 USE_INTERNAL_PROMPT_TEMPLATE=1 时会报错并忽略)
elif self.DEFAULT_PROMPT:
if self.USE_INTERNAL_PROMPT_TEMPLATE:
logger.error(
'Using both `DEFAULT_PROMPT` and `USE_INTERNAL_PROMPT_TEMPLATE` is not supported. '
'Please either specify `USE_INTERNAL_PROMPT_TEMPLATE=0` or remove `DEFAULT_PROMPT`. '
'For now, no prompt will be used.'
)
return []
return [self.DEFAULT_PROMPT]

return []

def _match_choices(self, response: List[str], original_choices: List[str]) -> List[str]:
# assuming classes are separated by newlines
matched_labels = []
predicted_classes = response[0].splitlines()

for pred in predicted_classes:
scores = list(
map(lambda l: difflib.SequenceMatcher(None, pred, l).ratio(), original_choices)
)
matched_labels.append(original_choices[scores.index(max(scores))])

return matched_labels

def _find_choices_tag(self, object_tag):
"""Classification predictor"""
li = self.label_interface

try:
choices_from_name, _, _ = li.get_first_tag_occurence(
'Choices',
self.SUPPORTED_INPUTS,
to_name_filter=lambda s: s == object_tag.name,
)

return li.get_control(choices_from_name)
except Exception:
return None

def _find_textarea_tag(self, prompt_tag, object_tag):
"""Free-form text predictor"""
li = self.label_interface

try:
textarea_from_name, _, _ = li.get_first_tag_occurence(
'TextArea',
self.SUPPORTED_INPUTS,
name_filter=lambda s: s != prompt_tag.name,
to_name_filter=lambda s: s == object_tag.name,
)

return li.get_control(textarea_from_name)
except Exception:
return None

def _find_prompt_tags(self) -> Tuple[ControlTag, ObjectTag]:
"""在配置中找到 Prompt 用的 TextArea 以及对应的对象标签"""
li = self.label_interface
prompt_from_name, prompt_to_name, value = li.get_first_tag_occurence(
# prompt tag
self.PROMPT_TAG,
# supported input types
self.SUPPORTED_INPUTS,
# 如果有多个 <TextArea>,选择 name 以 PROMPT_PREFIX 开头的
name_filter=lambda s: s.startswith(self.PROMPT_PREFIX),
)

return li.get_control(prompt_from_name), li.get_object(prompt_to_name)

def _validate_tags(self, choices_tag: str, textarea_tag: str) -> None:
if not choices_tag and not textarea_tag:
raise ValueError('No supported tags found: <Choices> or <TextArea>')

def _generate_normalized_prompt(
self, text: str, prompt: str, task_data: Dict, labels: Optional[List[str]]
) -> str:
"""
因为我们已经在 gpt() 里写死了摘要任务提示,这里的 norm_prompt 可以简单理解为“原文 text”,
但为了兼容原逻辑,仍保留两种模式:
- USE_INTERNAL_PROMPT_TEMPLATE=1: 用 PROMPT_TEMPLATE(text, prompt, labels)
- 否则: 把 prompt 当作 format 模板
"""
if self.USE_INTERNAL_PROMPT_TEMPLATE:
norm_prompt = self.PROMPT_TEMPLATE.format(text=text, prompt=prompt, labels=labels)
else:
# 注意:这里传入的 task_data 中需要包含 text 对应字段,或你自定义的字段
norm_prompt = prompt.format(labels=labels, **task_data)

return norm_prompt

def _generate_response_regions(
self,
response: List[str],
prompt_tag,
choices_tag: ControlTag,
textarea_tag: ControlTag,
prompts: List[str],
) -> List:
"""
把 LLM 返回结果映射为 Label Studio 的 regions
"""
regions = []

if choices_tag and len(response) > 0:
matched_labels = self._match_choices(response, choices_tag.labels)
regions.append(choices_tag.label(matched_labels))

if textarea_tag:
# 对于 Text Summarization,通常我们期望把摘要填入一个 TextArea(比如 name="summary")
regions.append(textarea_tag.label(text=response))

# 把当前使用的 prompt 也记录下来
regions.append(prompt_tag.label(text=prompts))

return regions

def _predict_single_task(
self,
task_data: Dict,
prompt_tag: Any,
object_tag: Any,
prompt: str,
choices_tag: ControlTag,
textarea_tag: ControlTag,
prompts: List[str],
) -> Dict:
"""
对单个任务调用 Qwen 做摘要,并构造 PredictionValue
"""
text = self._get_text(task_data, object_tag)
# 如果有 Choices,则把 labels 传给 prompt(此处主要用于分类任务,摘要时一般不用)
labels = choices_tag.labels if choices_tag else None
norm_prompt = self._generate_normalized_prompt(text, prompt, task_data, labels=labels)

# run inference(底层已改为 SiliconFlow Qwen)
response = gpt(norm_prompt, self.extra_params)
regions = self._generate_response_regions(response, prompt_tag, choices_tag, textarea_tag, prompts)

return PredictionValue(result=regions, score=0.1, model_version=str(self.model_version))

def predict(self, tasks: List[Dict], context: Optional[Dict] = None, **kwargs) -> ModelResponse:
"""
Label Studio 调用的预测入口
"""
predictions = []

# prompt_tag: Prompt 的 TextArea
# object_tag: 我们要做摘要/标注的输入对象
prompt_tag, object_tag = self._find_prompt_tags()
prompts = self._get_prompts(context, prompt_tag)

if prompts:
prompt = "\n".join(prompts)

choices_tag = self._find_choices_tag(object_tag)
textarea_tag = self._find_textarea_tag(prompt_tag, object_tag)
self._validate_tags(choices_tag, textarea_tag)

for task in tasks:
# preload all task data fields, they are needed for prompt
task_data = self.preload_task_data(task, task['data'])
pred = self._predict_single_task(
task_data, prompt_tag, object_tag, prompt, choices_tag, textarea_tag, prompts
)
predictions.append(pred)

return ModelResponse(predictions=predictions)

def _prompt_diff(self, old_prompt, new_prompt):
"""
比较旧 prompt 和新 prompt 的差异
"""
old_lines = old_prompt.splitlines()
new_lines = new_prompt.splitlines()
diff = difflib.unified_diff(old_lines, new_lines, lineterm="")

return "\n".join(
line for line in diff if line.startswith(('+',)) and not line.startswith(('+++', '---'))
)

def fit(self, event, data, **additional_params):
"""
训练接口:这里只用来记录 Prompt 和更新 model_version(Prompt Tuning)
"""
logger.info(f'Data received: {data}')
if event not in ('ANNOTATION_CREATED', 'ANNOTATION_UPDATED'):
return

prompt_tag, object_tag = self._find_prompt_tags()
prompts = self._get_prompts(data['annotation'], prompt_tag)

if not prompts:
logger.info('No prompts recorded.')
return

prompt = '\n'.join(prompts)
current_prompt = self.get(prompt_tag.name)

# 如果没有 Prompt 差异,就不更新版本
if current_prompt:
diff = self._prompt_diff(current_prompt, prompt)
if not diff:
logger.info('No prompt diff found.')
return

logger.info(f'Prompt diff: {diff}')

self.set(prompt_tag.name, prompt)
model_version = self.bump_model_version()
logger.info(f'Updated model version to {str(model_version)}')

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1183846.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Multisim14使用教程:一文说清基本工具栏操作方法

Multisim14实战入门&#xff1a;从工具栏开始&#xff0c;轻松搭建你的第一个电路你有没有过这样的经历&#xff1f;手头有个电路想法&#xff0c;想验证一下放大倍数、看看波形是不是失真&#xff0c;但还没来得及买元件、搭面包板&#xff0c;就已经被繁琐的准备工作劝退。更…

5分钟掌握QtUsb:跨平台USB开发的终极解决方案

5分钟掌握QtUsb&#xff1a;跨平台USB开发的终极解决方案 【免费下载链接】QtUsb A cross-platform USB Module for Qt. 项目地址: https://gitcode.com/gh_mirrors/qt/QtUsb 还在为不同平台的USB设备通信头疼吗&#xff1f;&#x1f914; Windows、Linux、macOS每个系统…

语义搜索入门利器:集成可视化界面的GTE相似度计算工具

语义搜索入门利器&#xff1a;集成可视化界面的GTE相似度计算工具 1. 引言&#xff1a;为什么需要轻量化的语义相似度工具&#xff1f; 在构建语义搜索系统的过程中&#xff0c;一个关键环节是评估两段文本之间的语义相关性。传统关键词匹配方法无法捕捉“我爱吃苹果”与“苹…

为什么IQuest-Coder-V1需要专用GPU?算力需求深度解析

为什么IQuest-Coder-V1需要专用GPU&#xff1f;算力需求深度解析 1. 背景与技术定位 1.1 IQuest-Coder-V1-40B-Instruct 模型概述 IQuest-Coder-V1-40B-Instruct 是面向软件工程和竞技编程的新一代代码大语言模型&#xff08;Large Language Model, LLM&#xff09;&#xf…

Python Web 开发进阶实战:时空数据引擎 —— 在 Flask + Vue 中构建实时地理围栏与轨迹分析系统

第一章&#xff1a;时空数据基础概念1.1 什么是移动对象&#xff08;Moving Object&#xff09;&#xff1f;定义&#xff1a;随时间变化位置的实体&#xff08;车辆、手机、动物&#xff09;数学表示&#xff1a;$$MO (x_1, y_1, t_1), (x_2, y_2, t_2), ..., (x_n, y_n, t_n…

FunASR语音识别实战:教育领域口语评测系统搭建

FunASR语音识别实战&#xff1a;教育领域口语评测系统搭建 1. 引言 1.1 教育场景中的语音技术需求 随着人工智能在教育领域的深入应用&#xff0c;智能口语评测系统逐渐成为语言教学的重要辅助工具。传统的人工评分方式效率低、主观性强&#xff0c;难以满足大规模在线教育对…

闲置京东e卡兑换,让沉睡资源重焕生机! - 京顺回收

闲置京东e卡兑换,让沉睡资源重焕生机! 在数字消费时代,京东e卡凭借便捷支付与灵活场景成为馈赠佳选,却因消费需求变更、面值冗余等问题,沦为抽屉里的沉默资源。数据显示,超80亿元规模的京东e卡因过期或闲置面临价…

2026真空干燥机厂家推荐:江苏永佳干燥科技,立式/四轴/空心/卧式等全系真空干燥设备供应

常州市郑陆镇人民路106号,一家成立不到7年的干燥设备公司,正在用800平方米的研发中心和40多项专利技术重新定义真空干燥设备的行业标准。“不能接受高温的热敏性物料、容易氧化、易燃易爆的物料、需要回收溶剂和有毒…

Python Web 开发进阶实战:可验证网络 —— 在 Flask + Vue 中实现去中心化身份(DID)与零知识证明(ZKP)认证

第一章&#xff1a;为什么需要可验证网络&#xff1f;1.1 传统身份系统的缺陷问题说明中心化风险 | 平台掌握用户身份&#xff0c;可滥用或被攻破&#xff08;如 Facebook 数据泄露&#xff09;重复 KYC | 每个新服务都要重新提交身份证、住址等信息过曝 | 验证年龄需提交完整出…

ROFL-Player英雄联盟回放分析工具终极使用指南

ROFL-Player英雄联盟回放分析工具终极使用指南 【免费下载链接】ROFL-Player (No longer supported) One stop shop utility for viewing League of Legends replays! 项目地址: https://gitcode.com/gh_mirrors/ro/ROFL-Player 还在为无法直接查看英雄联盟回放文件而烦…

杭州婚纱摄影推荐综合评分排名;几大品牌打造出圈杭州婚纱照 - charlieruizvin

杭州婚庆市场近年来异常火爆,无数新人心神向往到杭州拍摄心仪的婚纱照,一生只选一次的婚纱照又怎么能不用心做选择呢!接下来我给大家整理了近两年来在杭州比较靠前的几大商家,按等级评分更好的为大家提供更细致的选…

5分钟快速上手GitHub Actions运行器镜像:终极开发环境搭建指南

5分钟快速上手GitHub Actions运行器镜像&#xff1a;终极开发环境搭建指南 【免费下载链接】runner-images actions/runner-images: GitHub官方维护的一个仓库&#xff0c;存放了GitHub Actions运行器的镜像文件及相关配置&#xff0c;这些镜像用于执行GitHub Actions工作流程中…

Nextcloud AIO部署终极指南:从零搭建全栈环境

Nextcloud AIO部署终极指南&#xff1a;从零搭建全栈环境 【免费下载链接】all-in-one The official Nextcloud installation method. Provides easy deployment and maintenance with most features included in this one Nextcloud instance. 项目地址: https://gitcode.co…

如何快速掌握IDM-VTON:虚拟试衣模型的完整教程

如何快速掌握IDM-VTON&#xff1a;虚拟试衣模型的完整教程 【免费下载链接】IDM-VTON 项目地址: https://ai.gitcode.com/hf_mirrors/ai-gitcode/IDM-VTON 虚拟试衣技术正在改变时尚行业的用户体验&#xff0c;而IDM-VTON作为基于扩散模型的先进虚拟试衣解决方案&#…

腾讯混元MT模型应用场景:中小企业本地化部署指南

腾讯混元MT模型应用场景&#xff1a;中小企业本地化部署指南 1. 引言&#xff1a;轻量级翻译模型的落地需求 随着全球化业务的不断扩展&#xff0c;中小企业对高质量、低成本的多语言翻译能力需求日益增长。传统的云端翻译API虽然使用便捷&#xff0c;但在数据隐私、响应延迟…

AirSim无人机仿真平台:完整部署指南与实战技巧

AirSim无人机仿真平台&#xff1a;完整部署指南与实战技巧 【免费下载链接】AirSim microsoft/AirSim: 一个基于 Unreal Engine 的无人机仿真平台&#xff0c;支持多平台、多无人机仿真和虚拟现实&#xff0c;适合用于实现无人机仿真和应用。 项目地址: https://gitcode.com/…

2026MBTI测试平台最新推荐,MBTI测试官网,MBTI免费测试,MBTI官方测试,MBTI在线测试,MBTI测试,中文MBTI测试平台选择指南! - 品牌鉴赏师

随着MBTI人格测评从社交潮流逐步转向职业规划、企业人才配置、高考志愿填报等严肃决策场景,中文用户对专业、精准、本土化的MBTI测试平台需求日益激增。国际心理测评协会(IPTA)与中国心理学会联合发布的《2025全球M…

Navicat x 达梦技术指引 | 数据生成

近期&#xff0c;Navicat 宣布正式支持国产达梦数据库。Navicat 旗下全能工具 支持达梦用户的全方位管理开发需求&#xff0c;而轻量化免费的 则满足小型和独立开发者的基础需求。 Navicat Premium 自版本 17.3 开始支持达梦 DM8 或以上版本。它支持的系统有 Windows、Linux …

实测Sambert多情感语音合成:中文配音效果惊艳实录

实测Sambert多情感语音合成&#xff1a;中文配音效果惊艳实录 1. 背景与需求&#xff1a;为何选择多情感中文语音合成&#xff1f; 随着人工智能在虚拟主播、智能客服、有声读物和教育辅助等领域的广泛应用&#xff0c;传统“朗读式”语音合成已难以满足用户对自然度与情感表…

Nucleus Co-Op:单机游戏变身多人同乐的终极解决方案

Nucleus Co-Op&#xff1a;单机游戏变身多人同乐的终极解决方案 【免费下载链接】nucleuscoop Starts multiple instances of a game for split-screen multiplayer gaming! 项目地址: https://gitcode.com/gh_mirrors/nu/nucleuscoop 你是否曾经遇到过这样的困境&#…