更多精彩内容,欢迎观看:
工业组态 + LLM : 大模型技术引领传统工业软件创新与实践(上)
工业组态智能版 + 大模型的技术实现
整体技术架构
分成三层,从上至下分别是客户端层、代理层、模型层
- Clinet Layer(客户端层):组态智能化客户端部分,包括交互UI、支持的技能注册、服务端LLM Agent请求,对应LLM响应数据做工业组态功能点的执行。
- Agent Layer(代理层): LLM服务端代理,Web 请求与响应处理,包括数据流推送,还有Prompt Builder构造器。架构设计中,Prompt Builder也可以放Clinet层,这样研发效率会更高一些,但因为组态功能对LLM返回值及返回格式都有确定的要求,随意修改Prompt会影响系统稳定性。另一方面,LLM最终输入的Prompt,是由Agent Layer的Prompt Template + 用户在Clinet层输入的Input组合而成
- LLM Layer(模型层):LLM有很多,我们选择了双模型,产品化模型用阿里云千问大模型,测试和预研会使用OpenAI,毕竟是行业标杆,用OpenAI做Prompt响应控制,效果验证,能了解产品与LLM结合的上限做到什么程度。另一方面,OpenAI能力开放程度高,预研性的功能可以先基于OpenAI研发,等千问模型有了,可以做代替。
此架构仅只智能版部分,不包含工业组态本身的技术架构,组态本身和智能版通过MaliangEngine进行交互
Client Layer:核心智能交互实现
Client Layer主要的组成部分包括
- CopilotUI组件:实现应用生成、工业知识对话、工业组态脚本生成、智能绘图、智能属性修改、智能搜图/生图的自然语言指令交互入口。对应架构图中的Copliot UI
- LLM技能:包括对应的技能注册到Copilot Hub,技能调研服务API地址,调用技能后的后续action执行
- LLM技能action执行:根据LLM返回的数据,做对应的功能处理与执行
另外这一层会依赖于Maliang Engine
- Maliang Engine:工业组态搭建器引擎,核心组态技术能力都在这个模块中,通过window api提供给LLM Clinet Layer层调用。
CopilotUI组件:
这部分是用户最直观能感受到的UI交互层, 实现应用生成、工业知识对话、工业组态脚本生成、智能绘图、智能属性修改、智能搜图/生图的自然语言指令交互入口。对应架构图中的Copliot UI。比如LLM不同技能的返回消息展示组件,代码格式使用MSGUI-MD组件、文本内容用MSGUI-Text,应用缩略图使用MSGUI-APP,技能切换使用TrggerSelector组件等。
CopilotUI基于阿里小蜜提供的开源库ChatUI实现,这个Lib封装了对话框的交互事件和交互UI,极大提高了开发效率,基于ChatUI,自定义了text,code,app等几种消息体和对应的渲染函数。
交互入口代码,初始化Chat通过renderMessageContent、renderNavbar、Composer这三个函数去实现自定义的CopilotUI重构。
- renderNavbar:导航栏渲染
- messages: 所有发送和接收的消息响应
- renderMessageContent:消息渲染函数,可以根据消息的类型进行自定义UI渲染
- 消息格式:{ type, position,content}
- onSend:输入框发送消息事件
- Composer:自定义输入框
importChat, { Bubble, useMessages } from'@chatui/core'; <ChatrenderNavbar={renderNavbar} messages={messages} renderMessageContent={renderMessageContent} onSend={() => {}} Composer={renderComposer} />;
对话交互,使用position,left | right来区分输入和返回消息类型,对于可以通过自定义message的type,来做不同消息类型的展示
// 如果在右侧,是用户输入的内容,使用 输入文本 + 技能类型做展示constrenderMessageContent= (msg) => { if (position==='right') { return ( <ReqBubblemsg={msg} /> ); } elseif (position==='left') { switch (type) { case'code': return<ResCodeBubblemsg={msg} />case'script': return<ResScriptBubblemsg={msg} />case'app': return<ResAppBubblemsg={msg} /> } } }
LLM技能HUB:
这是一个设计模式,用于解耦大模型相关增强的智能功能。在产品有非常多的地方可以用到LLM做智能化升级,如果每个结合LLM的功能都在各自模块中进行处理,会破坏原有代码工程的代码结构,不利于维护。所以通过CopliotHub模式,通过技能注册,注册信息都可以抽象为:后端服务地址、请求参数设置、LLM返回数据后的处理函数。通过Copliot HuB去做技能注册、LLM服务请求、技能执行的拆解,这部分也不会影响到底层的Maliang Engine(工业组态代码)
注册代码示意:
constvoidfn= ()=>{} copilothub.register(Widget.chat,voidfn,{ stream:ture}} copilothub.register(Widget.scripts,voidfn,{ stream:ture}}} copilothub.register(Widget.svg,drawBySVG,{ stream:ture}}} copilothub.register(Widget.appgen,createAPPByDSL,{ stream:ture}}} copilothub.register(Widget.props,ModifyPropsByID,{ stream:ture}}} copilothub.register(Widget.images,DrawMaterial,{ stream:ture}}}
通过注册技能,根据技能标识,copilothub会先请求llm serve,并处理stream返回,再通过执行第二个参数action,将llm的返回值导入,组态clinet会根据响应值,执行对应的动作。在执行动作时,所有基础方法封装在Maliang Engine中并通过window对象进行交互。
LLM技能执行动作
当收到模型的响应后,会进入注册的后项执行方法,如代码中drawBySVG智能绘图执行器、createAPPByDSL智能应用创建执行器、ModifyPropsByID智能属性修改执行器、DrawMaterial智能素材生成执行器。接下来介绍下具体功能实现链路。
ModifyPropsByID-处理智能属性修改:
实现过程:
1)CopliotHub注册技能和技能执行方法 2)构造Prompt 3)根据LLM返回值,执行绘图功能
注册技能和执行方法
copilothub.register(Widget.props,ModifyPropsByID,{ stream:ture}}}
Prompt构造:Prompt分为两个部分,input为用户实际输入的内容,PromptTemplate是后台构造的Prompt模板。实际传入LLM的内容为两部分的组合。
在智能属性修改功能中,PromptTemplate是这样写的:
defprops_prompt(input,ctx): example='''example: 输入示例1:帮我把字体大小改成12输出示例1:{ "fontSize":"12"} 输入示例2:帮我把名称改成组件001,边框颜色改成红色输出示例2:{ "name":"组件001" , "borderC":"red"} 输入示例3:请将透明度修改为0.5,位置修改为x:100,y:200输出示例3:{ "opacity":0.5 , "x":100, "y":200} 输入示例4:背景颜色改为黄色输出示例4:{"bgColor":"yellow"} '''exception=''' 异常处理: 如果用户想要修改的属性不再列表中,请返回: {{ "error": "支持修改的属性包括:文字内容,长度、宽度、边框大小、边框颜色、透明的、隐藏、显示、文字颜色、文字内容、字体大小" }} '''ret=f'''一个组件有如下属性。key表示属性英文名称,value表示属性中文名称,如果一个key对应多个多个中文名称,用|分割"name": "组件名称", "content": "文字内容|文字|内容", "w": "宽度 长度", "h": "高度", "x": "x位置", "y": "y位置", "z": "z位置", "borderW":"边框大小 | 边框粗细", "borderW":"边框", "visible": "显示 true|隐藏 false", "opacity": "透明度", "borderC": "边框颜色", "bgColor": "背景颜色", "color": "文字颜色", "fontSize": "字体大小 | 文字大小", "textAlign": "文字水平对齐方式", //左对齐left 居中对齐middle 右对齐right"verticalAlign": "文字垂直对齐方式", //上对齐top 居中对齐middle 下对齐bottom此外,系统包含一些上线文信息如下:{ctx} 接下来我会输入一句话,用来修改组件对应属性的值返回结果格式为json,对应内容是 {{ "英文key": "修改的数值" }} 请不要返回除了json以外的其他任何内容用户输入内容为:{input} 给你一些参考示例: {example} {exception} '''returnret{ w:100}
这部分的逻辑写在Agent层的,所以是python代码,这里现在做说明,便于串联整体功能实现。
Prompt说明:
- 用途的设置,明确告诉LLM需要它做什么,上下文是什么,处理内容的格式是什么,对返回格式的要求。
- 把用户的输入 input 作为整体Prompt的一部分,告诉LLM用户的需求是什么
- 小样本训练,few shot,给出一些具体的example帮助,能帮助LLM更精准的返回内容
- 给出exception,对异常情况进行描述,设置异常情况,控制异常下数据返回
- ctx信息可以包含些私有信息,比如用户当前设置的画布大小是多少,可以ctx中加入 画布大小为1920 * 1024的信息,这样的好处是,当用户输入为 “移动屏幕中间” 时,模型也能有正确的响应。
Prompt模板对应的请求的返回值示例:
返回的原始数据位raw部分,通常情况下是包含json格式的一个string, 但LLM并不能100%保证格式正确。由于返回值是stream,因此我们需要根据流推送状态是否完成,拿完整stream的返回值,并做正则校验,提取json string,在转换为json对象。
constresponse=awaitfetch(url, { method: 'POST', body: JSON.stringify(param), headers: { 'Content-Type': 'application/json', }, }); constencode=newTextDecoder('utf-8'); constreader=response.body.getReader(); while (true) { const { done, value } =awaitreader.read(); if (done) { break; } consttext=encode.decode(value); //多通道LLM有不同的处理方式if (llm===LLM.OpenAI) { raw+=text; } elseif (llm===LLM.TongYi) { raw=text; } } console.log('raw:', raw);
除了做json转换为,还需要做容错。这是一个不断调整Prompt,观察返回结果的过程,如果返回不符合预期,调整用词、语序、明确指示、提高example样本等,都有很大帮助,推荐大家去看下吴恩达的课程《prompt engineering》。
我在实际开发阶段,通过几个有效方式,提高了智能属性修改的成功率。
1:LLM调用时,把temperature参数尽量设低,这个参数对于LLM的意义是返回结果的多样性,而我们在做绘图和智能属性修改时,希望得到确定性的返回值,所以要设低。实际调试结果来看,openai设置为0比较稳定,千问模型设置为0.1,设置为0会出现奇怪的异常。
2:LLM经常出现的非预期错误,做优化、修正、容错,特别是千问模型,对返回值不稳定,这种方式非常有效
- 边框大小修改为20 -> LLM返回的结果有时为 {borderW:30},有时是 {borderW:'30px'}
- 背景颜色设置为红色 -> LLM返回结果可能为 {bgColor:red} 、 {backgroundColor:red} 、 {backgroundColor:红色}
- 把文字内容改成 hello world, -> LLM返回结果可能为 {内容:hello world}
- 把透明度改成90% , -> LLM返回结果可能为 {opacity:90%} , {opacity:90}
3:过滤不需要处理的属性
4:清楚和理解大模型本身的差异,如qwen和openai gpt的差异,如果后端LLM模型不同,真的LLM模型关键词的敏感程度,去微调Prompt模板,实现更好的效果
通过CopilotHub处理完结构LLM数据响应后,就进入到具体处理属性修改的回调函数,回调函数的执行逻辑,都写在代码注释中了
//hub注册的属性修改对应的回调函数copilothub.register(Widget.props,ModifyPropsByID,{ stream:ture}}} functionModifyPropsByID(props) { try { //MaliangEngine 获取当前界面选择的组件idconstid=MaliangEngine.firstSelectedObject.id; // availableProps是通过culAvailableProps方法做容错// 里面做的事情包括 1:过滤不需要处理的prop, 2:处理模型返回json对象的容错 3:对比node props对应的数值类型,做转换constavailableProps=culAvailableProps(jsonObject, id); //MaliangEngine 执行对应属性的修改window.MaliangEngine.editSingleNodeProps(props, id); } catch (e) { console.log('modifyPrposByComponentsId error:', e, 'at:', id, 'props:', props); thrownewError(e); } }
DrawmatchBySVG 自然语言智能绘图实现:
智能绘图的实现过程和智能属性修改类似。 过程:1)CopliotHub注册技能和技能执行方法 2)构造Prompt 3)根据LLM返回值,执行绘图功能
copilothub.register(Widget.svg,drawBySVG,{ stream:ture}}}
2)构造Prompt
defsvg_prompt(input): ret=f""" Asafrontendengineer, youarerequiredtotranslateChineserequestsprovidedbyusersintoSVGcodeandreturntheresult. YoumustclearlyspecifythetypeofshapeandensureitscompatibilitywiththeSVGversionorformatwhendrawingoneormorespecifiedshapes. Thedefaultsizeoftheshapeshouldbe200*200anddefaultcolorisgray. Responsesshouldbeasconciseaspossible, containingonlyoneSVGcodewithoutanyinstructions, explanations, orcomments. ThegeneratedcodeshouldhaveonlyonerootSVGnode,andshapeinit, notusetheStylepropertyandusetheViewboxpropertytodeterminethecontentoftherootelement. Hereisuserrequests--- {input} ---"""returnret
图形绘制英文使用的是代码生成模型,实际返回结果会比中文效果更好。
这里对于图形绘制,有很多种方法实现,我用了SVG模式,让LLM根据用户输入返回SVG代码
3)根据LLM返回的SVG,执行绘图功能。这里代码就不写了,比较琐碎,drawBySVG中处理SVG各种形状到工业组态协议的转换过程。
工业知识技能和工业脚本生产实现相对简单,不需要后项技能执行器 。逻辑都在Prompt的构造上, 大家看看Prompt就明白了
LLM工业知识 Prompt Template
defchat_prompt(input): ret=f'你是一个工业知识小助手,根据用户的输入,回答工业领域的问题,用专业的语气回答用户的问题。回答请使用中文, 用户的在---符号中 --- {input} --- 'returnret
LLM脚本生成 Prompt Template
defscript_prompt(input): ret=f""" You are a JavaScript engineer , only return code markdown format,onlyonefunction, functionnamecallhandle(data) , data表示输入值. 用户的代码需求是: --- {input} ---""" returnret
最后一个是应用生成。这里我尝试首先尝试用LLM实现匹配,Prompt如下,list数据是通过应用平台部分模板数据构造而成,这里是举个例子
app_recommend_propmt='''你是一个推荐系统,我有一些应用模板,请根据输入,给我返回最匹配的应用模板,仅返回数据,不需要有多余信息模板列表,list= [ {{'设备监控大屏': '工控、设备监控、大屏、展示'}}, {{'成品仓库': '仓储、成品仓库、实时监测,保障成品的安全,避免盗窃、损毁'}}, {{'汽车生产mini产线': 'mini产线,实时生成数据、状态查看'}}, {{'原料仓库': '仓储管控,原材料库存'}}, {{'磨装车间': '轴承、磨装车间、生产态势监控'}}, {{'热处理车间': '热处理、生产情况的监控'}}, {{'态势总览': '厂区生产态势、能源、监控'}}, {{'煤粉工艺流程图': '煤粉制备、煤矿、工艺、生产流程,生产状态'}}, {{'城市管网处理平台': '城市管理、城市管网、管道、监控'}}, {{'污水处理': '污水处理状态监控'}}, {{'发电流程': '水泥工厂中电力管控流程图'}}, {{'PLC远程控制2': '远程对PLC控制'}}, {{'电力接线图': '电路元件、连接、电气关系'}}, {{'组态管道示例': '网、管、场景'}}, {{'天然气态势监控': '厂区用气,能耗管理'}}, {{'能源可视化': '能源消耗、能耗管理、节能、绿色'}}, {{'重点设备对比': '设备、监控'}}, {{'设备看板': '设备、看板'}}, {{'厂区用气监控': '气体、能源消耗、能耗管理、节能、绿色'}}, {{'厂区耗电监控': '电能、能源消耗、能耗管理、节能、绿色'}}, {{'厂区耗水监控': '水耗、用水、能源消耗、能耗管理、节能、绿色'}}, {{'PLC远程操作': 'PLC、远程、控制、反控'}}, {{'产线andon屏': '生产线的状态、故障、产线稳定'}}, {{'区域设备监控': '工位、产线、生成监控'}}, {{'总装车间-四合一': '汽车行业、总装车间、生成'}}, {{'总装车间-三合一': '汽车行业、总装车间、生成'}}, {{'车间能源消耗': '工厂、车间、能源、消耗'}}, {{'工控屏': '工业控制、数据展示'}}, {{'焊装车间-工位图': '汽车行业、焊装车间、工位状况'}}, {{'焊装车间-1024屏': '汽车行业、焊装车间、工位状况'}}, {{'焊装车间-1280屏': '汽车行业、焊装车间、工位状况'}}, {{'焊装车间纵览': '汽车行业、焊装车间、工位状况'}}, {{'立体车库-亮色版': '汽车行业、立体车库'}}, {{'立体车库-暗色版':'汽车行业、立体车库'}}, ..... 更多模板从数据库中获取 ] list中元素格式{'名称':'关键词1'}} input为用户输入的模板要求, input= {input}, 请从list中选出1-4个最匹配的模板作为返回返回的格式为名称组成的数组输入示例:给一个立体车库的模板输出示例:['立体车库-亮色版','立体车库-暗色版','成品仓库'] ''';
如果不通过LLM实现,这个逻辑通过代码实现,可以采用高低位的算法匹配和抽样获取,但缺点是,LLM可以提取和获得相近的意思,而匹配算法不能。比如输入节能,LLM可以匹配到能耗、绿色等关键词,而算法方法就无法匹配。示例代码如下:
// 智能应用推荐exportfunctionapp_recommend_base(appList,input) { constsource=Array(appList.length).fill(0); for (constciofinput) { for (leti=0; i<appList.length; i++) { constapp=appList[i]; conststr=JSON.stringify(app).replace('name', '').replace('key', '').replace("'", ''); conststrarr=str.split(''); for (constcoofstrarr) { if (ci===co) source[i]++; } } } consthigh= []; constlow= []; appList.forEach((item, i) => { consts=source[i]; item.s=source[i]; if (s>0) { high.push(item); } else { low.push(item); } }); high.sort((a, b) =>b.s-a.s); letret=high.slice(0, 4); constlack=Math.max(0, 4-high.length); // 抽签for (leti=0; i<lack; i++) { constindex=Math.floor(Math.random() *low.length); ret.push(low[index]); deletelow[index]; } ret=ret.map((item) =>item?.name); returnret; }
LLM Agent :模型服务端,实现模型代理和Prompt Engineering
这一层是工业组态智能版对应LLM能力的服务端,改服务端和原先工业组态的服务端分成2个后端应用,因为工业组态会有边缘无互联网独立部署的情况,智能版也并非产品的默认标配,所以拆成新的应用。
这层主要处理LLM的通道设置,代理LLM的请求和响应。实现一个web api serve
技术选型:python、 django、对应模型的后端SDK,如(Openai-python-sdk、dashscope-python-sdk)
PromptBuilder在Agent Layer层的实现可以保证系统稳定性稳定。上一节已经详细介绍了具体Prompt的内容,所以这里不再重复。
Agent建设有几处值得分享的建议:
1:为了提高开发效率,在开发阶段可以将Prompt构造放在客户端,serve提供如qianw/ turbo/ davinci/作为通用接口。但在预发和线上阶段,Prompt对LLM返回值影响巨大,不能随意修改,这部分代码放在serve段会更稳定。
urlpatterns= [ path('chat/', api.chat, name='chat'), path('image/', api.image, name='image'), path('svg/', api.svg, name='svg'), path('script/', api.script, name='script'), path('props/', api.props, name='props'), path('qianw/', api.qianw, name='qianw'), path('turbo/', api.turbo, name='turbo'), path('davinci/', api.davinci, name='davinci'), ]
2: temperature参数决定LLM返回值的多样性,提高稳定可以导致更多的随机性。对基于事实的回答或者想控制返回值的稳定性,需要把temperature设低。比如脚本生成、绘图、智能属性修改Prompt对应的温度,就需要竟可能降低。知识问答,可以把temperature设高些增加互动性。目前qwen模型不支持这个参数。通义对随机性的控制可以使用top_p,这个参数和temperature的原理并不完全相同,但作用类似。top_p:Nucleus Sampling采样算法,用大白话讲就是采取概率分布较高的集合中随机返回结果。
3:content_type='text/event-stream' 需要设置,匹配安全和风控产品重写fetch函数造成异常
4:LLM调用,通过stream返回数据返回的实践是比较通用的逻辑,可以借鉴。这里已django、openai阿里通义qianwen模型举例
defresp(data,message=''): resp= { 'code': 200ifmessage==''else500, 'data': data, 'message':'succeed'ifmessage==''elsemessage, 'success': Trueifmessage==''elseFalse, 'description': '' } returnrespdefstream_response(response): forchunkinresponse: cho=chunk['choices'][0] print(f'chunk[\'choices\']: {cho}') chunk_message=chunk['choices'][0] #extractthemessagetext=chunk['choices'][0]['text'] yieldtextdefchat(request): ifrequest.method=="POST": req=json.loads(request.body) input=req["prompt"] else : returnJsonResponse(resp('','prompt undefined')) response=openai.Completion.create( model="text-davinci-003", prompt=prompt_builder.chat_prompt(input), max_tokens=512, temperature=0.8, timeout=60, stream=True, ) returnStreamingHttpResponse(stream_response(response), content_type='text/event-stream', status=200) defchat(request): ifrequest.method=="POST": req=json.loads(request.body) input=req["prompt"] else : returnJsonResponse(resp('','prompt undefined')) response=Generation.call( model='qwen-v1', prompt=prompt_builder.chat_prompt(input), stream=True, top_p=0.8) returnStreamingHttpResponse(stream_response(response), content_type='text/event-stream', status=200)
5:服务部署的网络选择:阿里通义qwen模型网络情况好,接口稳定,速度快,国内任何云服务器的region都可以正常访问。开发日常环境可以使用openai的gpt模型做对比测试,openai模型使用稍微复杂些,网上有很多资料,这里我就不做介绍了。
6 在开发初期,可以使用Google Colab服务做开发测试。与Jupyter的使用方式一样,优点是环境配置非常简单、方便。
LLM Layer:模型层,模型的选择和经验
这一层就是使用成熟的大模型,作为智能化应用最重要的部件,可以比喻成心脏,好的LLM事半功倍。
国外的LLM
- OpenAI GPT3、3.5、4
- Meta LLaMa
- Google PaLM、LaMDA
国内的LLM
- 阿里:通义大模型 内测
- 百度:文言一心
- 腾讯:HunYuan 未发布
网上模型资料很多,工业组态智能版接入2个模型,产品化LLM使用阿里通义模型,开发态使用通义模型和openai模型做对比、评估、测试。
通义模型:通过从DashScope提供的MaaS获取,注册API,申请权限:https://dashscope.aliyun.com/
DashScope
OpenAI: 使用davinci-003模型 + gpt-3.5-turbo模型。其中davinci-003是GPT3.0模型,我们的场景中单轮对话较多,并且考虑fine-turing场景,3.0模型是唯一支持的模型,且速度快,费用低。
总结
大模型、ChatGPT是作者每天都在用的产品,也经常会想结合LLM做什么能让生活和工作带来改变。工业组态智能版发布后,也有许多工业企业申请试用,和我们沟通工业应用场景。
在过去的两个月,作者在工业组态场景花费了一点时间,这周用了两天的时间把产品与技术的一点心得体会写出来,希望能帮到正在使用计划和正在使用大模型的同学。