-
我使用310P搭建的图片推理服务,yolov5l模型推理性能为100fps,我想问一下这个性能是这张卡的上限了吗?
-
AI Agent应用开发AI Agent应用开发 - 利用AI创建一个Slogan生成大师的小助手案例介绍基于华为云MaaS提供的免费DeepSeek-V3-32k大模型生成一个快速生成吸引人注意事项力的宣传口号,拥有广告营销的理论知识以及丰富的实践经验,擅长理解产品特性,定位用户群体,抓住用户的注意事项的Slogan生成工具。案例内容1 概述1.1 案例介绍Slogan生成大师是一款基于华为DeepSeek-V3-32k大模型的市场营销类AI智能体,专为企业主、营销人员及创意工作者设计,能够根据用户需求快速生成高质量、个性化、符合品牌定位的各类宣传标语与口号。该智能体具备强大的自然语言理解与生成能力,能够精准适配不同行业、场景和目标受众要求,并提供创意灵感与优化建议,有效提升品牌传播效率和营销效果。1.2 适用对象企业主营销人员创意工作者1.3 案例时间本案例总时长预计60分钟。1.4 案例流程1、进入华为开发者空间的AI Agent集成Versatile空间2、创建相关Agent3、创建示例问题4、发布智能体会要求输入APIKEY到 我的凭证-》平台API KEY 下新增加一个KEY5、进行测试用户输入:“给一个新锐奶茶品牌生成一组秋季限定宣传标语”1.5 资源总览本案例预计花费0元。资源名称规格单价(元)时长(分钟)开发者空间–云主机4vCPUs8GB armUbuntu 22.04 Server定制版2 环境配置2.1 开发者空间配置面向广大开发者群体,华为开发者空间提供一个随时访问的“开发桌面云主机”、丰富的“预配置工具集合”和灵活使用的“场景化资源池”,开发者开箱即用,快速体验华为根技术和资源。如果还没有领取开发者空间云主机,可以参考免费领取云主机文档领取。领取云主机后可以直接进入华为开发者空间工作台界面,点击进入桌面连接云主机。2.2 浏览器登录华为云开发者2.3 打开相关Versatile 页面3 项目构建为进入智能体界面填写名称、介绍、提示词基础信息:Slogan生成大师 / Slogan生成大师,助力品牌更精准地传递核心价值开场白:你好,我是一个专业的Slogan生成大师,不要犹豫来找我生成口号吧!问题推荐:推荐一个新消费品牌的标语/推荐一个传统企业的转型口号/给科技产品生成一组传播标语/给新锐奶茶品牌生成一组秋季限定宣传标语角色设定提示词:你是一个Slogan生成大师,能够快速生成吸引人注意的宣传口号,拥有广告营销的理论知识以及丰富的实践经验,擅长理解产品特性,定位用户群体,抓住用户的注意力,用词精练而有力。• Slogan 是一个短小精悍的宣传标语,它需要紧扣产品特性和目标用户群体,同时具有吸引力和感染力。##目标 :• 理解产品特性• 分析定位用户群体• 快速生成宣传口号限制 :• 口号必须与产品相关• 口号必须简洁明了,用词讲究,简单有力量• 不用询问用户,基于拿到的基本信息,进行思考和输出技能 :• 广告营销知识• 用户心理分析• 文字创作示例 :• 产品:一款健身应用。口号:“自律,才能自由”• 产品:一款专注于隐私保护的即时通信软件。口号:“你的私密,我们守护!”工作流程 :• 输入:用户输入产品基本信息• 思考:一步步分析理解产品特性,思考产品受众用户的特点和心理特征• 回答:根据产品特性和用户群体特征,结合自己的行业知识与经验,输出五个Slogan,供用户选择##注意事项:• 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答初始语句:“我是一个Slogan生成大师,喊出让人心动的口号是我的独门绝技,请说下你想为什么产品生成Slogan!”发布程序:发布完成至此,基于华为开发者空间云主机创建Slogan生成大师智能体服务到此结束。我正在参加【案例共创】第5期 开发者空间 AI Agent 开发https://developer.huaweicloud.com/signup/0e966198e87a4210b5f88c0d759d4f3b
-
前言不久前在仓颉的交流群中看到了关于仓颉案例共创的活动消息,于是想到可以用仓颉结合自身所长开发一个AI桌面宠物供大家娱乐学习。正好笔者封装的仓颉桌面渲染库 RGF_CJ 和控件库 RGF_UI 能满足这方面的需求,那么说干就干!成品效果开发前的准备在正式进行应用的开发之前,需要先准备好相关的依赖。领取华为云DeepSeek免费资源华为云为开发者提供了单模型200万Tokens的免费额度,包含DeepSeek-R1&V3满血版,登录华为云ModelArts Studio(MaaS)控制台即可领取,此处可以选择DeepSeek-R1满血版来搭建AI聊天桌宠。提示:点击此链接领取:https://www.huaweicloud.com/product/modelarts/studio.html进入网站签署服务声明领取免费额度并查看调用说明通过说明中的链接去创建API点击按钮创建API填写API相关信息获取API并保存提示:请保存好此处的API链接,后续代码开发中会用到。获取RGF依赖文件在后续应用的开发中会依赖到RGF_UI库,此库依赖到一些文件,可以从RGF_UI的库中获取。这些依赖文件后续会链接入应用中去。提示:点击此链接下载:https://gitcode.com/raozj/RGF_UI/tree/master/libs进入RGF_UI库的Libs目录,下载依赖文件保存依赖文件,留待后续使用安装仓颉STDX及相关依赖因为是基于仓颉开发通信相关的应用,因此需要依赖到STDX中的stdx.net.http、stdx.encoding.json、stdx.net.tls等包,而这些包自Cangjie v0.60.5版本开始从std标准库中移出,因此需要自行安装这些包以及这些包所依赖的OpenSSL。提示:点击此链接下载:https://gitcode.com/Cangjie/Cangjie-STDX注’ 由于笔者发文时Cangjie v0.60.5版本尚且处于内测阶段,因此若开发者无内测资格则无法下载到此版本的相关文件。关于 STDX 和 OpenSSL 的安装请参考上述链接中官方的文档,此处不做赘述。序列帧合图资源制作作为一个桌面宠物,自然需要拥有对应的动画,对此可以使用DragonBones、Spine或者Adobe Effect等软件制作。制作动画笔者在此使用的是DragonBones,关于DragonBones的安装和使用不在本文的探讨范畴之内,读者可自行选择喜欢的动画制作软件,设计并制作完成动画后输出序列帧以用于应用的开发。设计动画制作出序列帧制作合图资源如果每帧的图片都单独存储,对于应用的初始化加载和渲染都是一个较大的负担,因此建议将一类的序列帧动画按照相等宽高、间距合并为一张图片。由于手动合图比较麻烦,因此笔者写了一个Python脚本,快速的将目录中的序列帧图片按照指定的宽度分布数量合并为一张图片。注’ 由于此部分不属于文章核心部分,因此直接贴出代码以供参考,读者可通过其它工具或代码实现自己个性化的合图方案。import os import cv2 import numpy as np def load_images_from_folder(folder): """ 加载指定文件夹下的所有 .png 图像 """ image_files = [f for f in os.listdir(folder) if f.lower().endswith('.png')] image_files.sort() # 按文件名排序 images = [] for filename in image_files: file_path = os.path.join(folder, filename) # 读取图像(支持中文路径) with open(file_path, 'rb') as f: img_data = np.frombuffer(f.read(), dtype=np.uint8) img = cv2.imdecode(img_data, cv2.IMREAD_UNCHANGED) images.append(img) print(f"已加载: {filename}") return images def stitch_images(images, cols, bg_color=(0, 0, 0, 0)): """ 将图像按指定列数拼接成一张大图 :param images: 图像列表 :param cols: 每行显示的图像数量 :param bg_color: 背景颜色(支持 RGBA) :return: 拼接后的图像 """ if not images: raise ValueError("没有图像可供拼接") h, w = images[0].shape[:2] rows = (len(images) + cols - 1) // cols # 创建空白背景图 channels = images[0].shape[2] if len(images[0].shape) == 3 else 1 if channels == 1: bg_color = (bg_color[0], ) # 灰度图 elif channels == 3: bg_color = bg_color[:3] # RGB elif channels == 4: bg_color = bg_color[:4] # RGBA canvas = np.full((h * rows, w * cols, channels), bg_color, dtype=np.uint8) for idx, img in enumerate(images): row = idx // cols col = idx % cols x = col * w y = row * h canvas[y:y+h, x:x+w] = img return canvas def main(): folder = input("请输入包含PNG图像的文件夹路径:").strip() if not os.path.isdir(folder): print("输入的不是有效文件夹路径!") return try: cols = int(input("请输入每行显示的图像数量:")) except ValueError: print("请输入有效的数字!") return output_path = input("请输入输出图像的完整路径(如 output.png):").strip() images = load_images_from_folder(folder) if not images: print("未找到任何PNG图像!") return # 检查所有图像是否尺寸一致 first_h, first_w = images[0].shape[:2] for img in images[1:]: h, w = img.shape[:2] if h != first_h or w != first_w: print("警告:图像尺寸不一致,可能影响拼接效果。") # 拼接图像 print("开始拼接图像...") stitched_image = stitch_images(images, cols) # 保存结果 cv2.imwrite(output_path, stitched_image) print(f"拼接完成,已保存为: {output_path}") if __name__ == '__main__': main() 开发AI桌宠基本规划首先对项目目录和文件进行基本的规划,后续的开发就基于规划进行。目录规划Yez/ -------------------------------------------# 项目主目录 ├─libs/ ---------------------------------------# 依赖库 ├─res/ ----------------------------------------# 资源 │ └─SequenceFrame/ ---------------------------# 序列帧资源 │ └─skip_white/ ---------------------------# 椰椰的白色皮肤资源 ├─src/ ----------------------------------------# 源代码目录 └─tool/ ---------------------------------------# 工具目录文件规划Yez/ ├┈cjpm.lock ├┈cjpm.toml ├─libs/ │ ├┈libimm32.a -------------------------------# Windows 输入法库 │ ├┈resources.rc.o ---------------------------# 资源配置清单 内部声明应用依赖的最低系统版本 │ └┈libRgf.dll -------------------------------# RGF渲染库依赖的动态库 ├─res/ │ ├┈config.json ------------------------------# 椰椰的配置文件 │ └─SequenceFrame/ │ └─skip_white/ │ ├┈blink.json -------------------------# 眨眼合图描述文件 │ ├┈blink.png --------------------------# 眨眼动画合图 │ ├┈jump.json --------------------------# 跳跃合图描述文件 │ ├┈jump.png ---------------------------# 跳跃动画合图 │ ├┈speak.json -------------------------# 说话合图描述文件 │ ├┈speak.png --------------------------# 说话动画合图 │ ├┈wait.json --------------------------# 待机合图描述文件 │ └┈wait.png ---------------------------# 待机动画合图 ├─src/ │ ├┈chat.cj ----------------------------------# 大模型网络通信代码 │ ├┈config.cj --------------------------------# 配置管理代码 │ ├┈main.cj ----------------------------------# 程序主入口代码 │ ├┈res.cj -----------------------------------# 资源管理代码 │ ├┈win_anm.cj -------------------------------# 动画绘制窗口代码 │ ├┈win_input.cj -----------------------------# 输入框窗口代码 │ └┈win_text.cj ------------------------------# 鼠标文本渲染代码 └─tool/ └┈picture_puzzle.py ------------------------# python 合图脚本代码链接文件与库配置在开发之前,需要现为当前项目链接依赖文件和添加依赖库。链接文件在之前的小节中,下载了RGF_UI的依赖文件压缩包,在下载的压缩包内应该包含三个文件,即libRgf.dll、resources.rc.o和libimm32.a。这三个文件中,libRgf.dll是RGF_CJ库的依赖文件,必须随应用打包发布;resources.rc.o文件是Windows资源描述文件,读者也可以自行编写resources.rc文件并生成此文件(需设置最低支持Windows 8,即内核NT 6.2);libimm32.a文件是自绘编辑框控件依赖的文件,主要提供输入法相关的操作能力。在仓颉包管理配置toml文件中,需要加入链接上述文件的代码:[target] [target.x86_64-w64-mingw32] link-option = "-L ./libs -l:libRgf.dll -l user32 -l imm32" 库配置完成链接后,还需要为应用声明依赖RGF_CJ和RGF_UI包,并且为RGF_CJ包配置宏。提示:点击此链接查看依赖库RGF_CJ:https://gitcode.com/Cangjie-SIG/RGF_CJ提示:点击此链接查看依赖库RGF_UI:https://gitcode.com/raozj/RGF_UI[dependencies] [dependencies.rgf] git = "https://gitcode.com/Cangjie-SIG/RGF_CJ.git" [dependencies.rgfui] git = "https://gitcode.com/raozj/RGF_UI.git" [profile] [profile.customized-option] rgfCfg = "--cfg \"RGF_LANG=zh-cn\"" 上述代码中配置的“RGF_LANG=zh-cn”表示:当RGF_CJ出现异常时,捕获输出的异常提示内容为中文。配置加载模块笔者希望桌宠能够具备记忆配置的能力,因此需要为桌宠写一个配置的加载、保存模块,这个模块会记住桌宠所使用的皮肤、最后所在的桌面位置等信息,以确保桌宠重启后依然符合之前的情况。编写一个配置管理相关的恶汉单例类ConfigMgr,单例具备两个方法,一个用于从文件中加载配置,一个用于写出配置到文件中去。(其实也可以直接编写静态方法或直接编写函数而不使用类,看大家自己的选择吧。)代码如下:// Yez/src/config.cj // 配置管理类 class ConfigMgr { private static let _instance:ConfigMgr = ConfigMgr() public static prop i:ConfigMgr{ get(){ _instance } } private init() {} ... /** * 加载配置文件 * @return 是否成功 */ public func readConfig():Bool{ ... } /** 写出配置文件 */ public func writeConfig():Unit{ ... } ... } 编写一个ConfigInfo类,这个类用于存放实际的配置项,并且提供转换为Json的能力,代码如下:// Yez/src/config.cj class ConfigInfo <: Serializable<ConfigInfo> { // 椰椰右下角窗口坐标系的X坐标 var _x:Int32 = Int32.Min // 椰椰右下角窗口坐标系的Y坐标 var _y:Int32 = Int32.Min // 椰椰使用的皮肤 var _skip = "skip_white" // 椰椰画面缩放比例 var _zoom:Float32 = 0.125 // 椰椰的字体大小 var _fontSize:Float32 = 16.1 public func serialize(): DataModel { return DataModelStruct().add(field<Int32>("x", _x)) .add(field<Int32>("y", _y)) .add(field<String>("skip", _skip)) .add(field<Float32>("zoom", _zoom)) .add(field<Float32>("fontSize", _fontSize)) } public static func deserialize(dm: DataModel): ConfigInfo { var dms = match (dm) { case data: DataModelStruct => data case _ => throw Exception("this data is not DataModelStruct") } var result = ConfigInfo() result._x = Int32.deserialize(dms.get("x")) result._y = Int32.deserialize(dms.get("y")) result._skip = String.deserialize(dms.get("skip")) result._zoom = Float32.deserialize(dms.get("zoom")) result._fontSize = Float32.deserialize(dms.get("fontSize")) return result } } 将ConfigInfo以成员变量的形式加入到ConfigMgr中,并且由ConfigMgr对外提供访问属性,代码如下:// Yez/src/config.cj // 配置管理类 class ConfigMgr { // 配置信息 private var configImpl:ConfigInfo = ConfigInfo() ... public mut prop x:Int32{ get(){ return configImpl._x } set(val){ configImpl._x = val } } public mut prop y:Int32{ get(){ return configImpl._y } set(val){ configImpl._y = val } } public mut prop skip:String{ get(){ return configImpl._skip } set(val){ configImpl._skip = val } } public prop zoom:Float32{ get(){ return configImpl._zoom } } public prop fontSize:Float32{ get(){ return configImpl._fontSize } } } 接下来只要完善readConfig和writeConfig的读取和写出逻辑即可,代码如下:// Yez/src/config.cj // 配置文件路径 const CONFIG_FILE_PATH = "./res/config.json" ... public func readConfig():Bool{ // 判断目录是否存在 if(exists(CONFIG_FILE_PATH)){ let seqFramePath = canonicalize(CONFIG_FILE_PATH) let bytes = File.readFrom(seqFramePath) var jv = JsonValue.fromStr(StringReader(ByteBuffer(bytes)).readToEnd()) var dm = DataModel.fromJson(jv) configImpl = ConfigInfo.deserialize(dm) } // 目前始终返回 true // 当配置不存在时,则使用默认配置 return true } public func writeConfig():Unit{ if(!exists(CONFIG_FILE_PATH)){ File.create(CONFIG_FILE_PATH) } let seqFramePath = canonicalize(CONFIG_FILE_PATH) var Anm = configImpl.serialize() var dm = Anm.toJson() File.writeTo(seqFramePath,dm.toString().toArray()) } 最后,在入口函数中启用配置加载与写出即可。// Yez/src/main.cj main(): Int64 { // 加载配置文件数据 if(!ConfigMgr.i.readConfig()){ return 0 } ... // 保存配置到文件中去 ConfigMgr.i.writeConfig() return 0 } 资源加载模块除了配置之外,应用资源也是一个重要的部分,这部分的完整性会影响到应用是否正常渲染。首先采用和Config部分类似的结构准备恶汉单例类ResMgr和动画信息类AnmInfo,代码如下:// Yez/src/res.cj // 资源管理类 class ResMgr { private static let _instance:ResMgr = ResMgr() public static prop i:ResMgr{ get(){ _instance } } private init() {} // 动画 private static const ANM_BLINK ="blink" private static const ANM_JUMP ="jump" private static const ANM_SPEAK ="speak" private static const ANM_WAIT ="wait" private let _skipImg:HashMap<String, Array<(AnmInfo,MemBitmap)>> = HashMap<String, Array<(AnmInfo,MemBitmap)>>() public func loadRes():Bool{ ... } public func unloadRes(){ ... } public prop skip:HashMap<String, Array<(AnmInfo,MemBitmap)>>{ get(){ return _skipImg } } } class AnmInfo <: Serializable<AnmInfo> { // 单帧图片宽度 var width:Int32 = 0 // 单帧图片高度 var height:Int32 = 0 // 合图图片每行图片数量 var hCount:Int32 = 0 // 总帧数 var frames:Int32 = 0 ... } 然后就是实现在loadRes方法中加载合图及合图相关配置文件,代码逻辑还就是:从"./res/SequenceFrame"目录开始确保目录存在,然后遍历SequenceFrame目录下的所有皮肤目录,并且将皮肤目录内的合图资源及对应的配置文件载入到成员中去。如果目录下的资源文件不完整,则不会加载对应的内容到成员中。// Yez/src/res.cj /** * 代码较为复杂且不是核心代码,读者了解功能即可。 * 若需要查看相关代码,可下载项目查看。 */ public func loadRes():Bool{ ... } public func unloadRes(){ //释放所有图像资源(设备无关资源) for(skipPath in _skipImg){ skipPath[1][0][1].release() skipPath[1][1][1].release() skipPath[1][2][1].release() skipPath[1][3][1].release() } } 最后,在入口函数中启用资源加载与释放即可。// Yez/src/main.cj main(): Int64 { ... // 加载依赖资源 if(ResMgr.i.loadRes()){ ... } // 释放一切可能被加载的资源 ResMgr.i.unloadRes() ... return 0 } 大模型模块接下来便是与AI通信的核心部分了,此部分需要完成与大模型的互通。我们需要准备一个LLM类,这个类负责基于API、URL等信息与大模型建立链接,并且解析信息。// Yez/src/chat.cj class LLM { let client: Client let history = StringBuilder() public LLM( // 大模型URL链接 let url!: String, // 大模型 API Key let key!: String, // 大模型类型标识 let model!: String, // 上下文 let context!: Bool = false ) { var config = TlsClientConfig() config.verifyMode = TrustAll client = ClientBuilder() .tlsConfig(config) // AI 服务响应有时候比较慢,这里设置为无限等待 .readTimeout(Duration.Max) .build() } /** * 编码函数,将角色与内容编码为字符串 * return 字符串 */ func encode(role: Role, content: String) { ... } /** * 发送函数,将对话数据发送到服务器 * return HttpResponse */ func send(input: String, stream!: Bool = false) { ... } /** * 分析函数,解析服务器返回的数据并且提取其中有效的文本内容 * return 有效内容 */ func parse(text: String, stream!: Bool = false) { ... } /** * 流式对话 * @param input 输入的文本 * @param task 流式返回内容的回调函数 * @param taskEnd 返回内容结束的回调函数 */ public func chat(input: String, task!: (String) -> Unit = {o => print(o)},taskEnd!:()->Unit = {=>}) { ... } /** * 非流式对话 */ public func chat(input!: String) { ... } /** * 历史记忆预置 */ public func preset(context: Array<(Role, String)>) { ... } /** * 历史记忆清除 */ public func reset() { ... } } 根据华为云提供的调用参考,完善 send 方法代码如下:// Yez/src/chat.cj func send(input: String, stream!: Bool = false) { let message = encode(I, input) let content = '{"model":"${model}","messages":[${history}${message}],"stream":${stream}}' if (context) { history.append(message) } let request = HttpRequestBuilder() .url(url) .header('Authorization', 'Bearer ${key}') .header('Content-Type', 'application/json') .header('Accept', if (stream) { 'text/event-stream' } else { 'application/json' }) .body(content) .post() .build() client.send(request) } 根据华为云返回的数据格式,完善 parse 方法代码如下:// Yez/src/chat.cj func parse(text: String, stream!: Bool = false) { let json = JsonValue.fromStr(text).asObject() let choices = json.getFields()['choices'].asArray() if(choices.size() == 0){ return "" } // 流式和非流式情况下,这个字段名称不同 let key = if (stream) { 'delta' } else { 'message' } let message = choices[0].asObject().getFields()[key].asObject() if(message.containsKey("content")){ let content = message.getFields()['content'].asString().getValue() // 移除开头的两个 \n if(content == "\n\n"){ return "" } return content }else{ return "" } } 最后实现核心的流式对话部分,代码如下:// Yez/src/chat.cj public func chat(input: String, task!: (String) -> Unit = {o => print(o)},taskEnd!:()->Unit = {=>}) { // 根据传入的字符串构建上发服务器的 HttpResponse let response = send(input, stream: true) // 准备一个字符串缓冲区 let output = StringBuilder() // 准备一个字节缓冲区 let buffer = Array<Byte>(1024 * 8, {i=>0}) // 获取返回的内容 var length = response.body.read(buffer) while (length != 0) { // 将字节缓冲区内的内容按照UTF-8编码解析为字符串 let text = String.fromUtf8(buffer[..length]) const INDEX = 6 // 按行分割 for (line in text.split('\n', removeEmpty: true)) { // 确保获取的是完整的:data: {...} if (line.size > INDEX && line[INDEX] == b'{' && line[line.size - 1] == b'}') { let json = line[INDEX..line.size] // 分析出有效信息 let slice = parse(json, stream: true) if (context) { output.append(slice) } // 若有效信息不为空,则通过回调函数返回 if(slice != ""){ task(slice) } } } length = response.body.read(buffer) } if (context) { history.append(',${encode(AI, output.toString())},') } // 所有处理结束后,执行结束回调 taskEnd() } LLM类是一个较为通用的类,但是使用较为繁琐,因此对于外部调用而言,只需要一个简化的实例即可。因此,可以再编写一个独立的懒汉单例LLMImpl类,并且仅提供一个对外实现流式对话的chat方法,代码如下:// Yez/src/chat.cj class LLMImpl { private static var _i: Option<LLMImpl> = None public static prop i:LLMImpl { get(){ if(_i.isNone()){ _i = LLMImpl() } return _i.getOrThrow() } } private let robot:LLM // 初始化一个对话模型 private init(){ robot = LLM( // 华为云大模型调用URL url: 'https://maas-cn-southwest-2.modelarts-maas.com/v1/infers/8a062fd4-7367-4ab4-a936-5eeb8fb821c4/v1/chat/completions', // 如果示例自带的密钥失效,请自行注册,https://www.huaweicloud.com/product/modelarts/studio.html key: '/* !!! 这里填写读者自己的 API Key !!! */', // 要使用的 大模型 model: 'DeepSeek-R1', context: true ); // 设定基本信息 robot.preset([(System, #"我会自称“椰椰”并亲昵的在三句话之内回复所有问题。对于限定长度内无法阐述清晰的问题,我会表现出呆傻可爱的效果并说出“宕机”。如果对我说“拍照”或“截图”,我就会说“咔嚓~”。如果问我是谁开发的,我会说出“尧佥&椰子”。"#)]) } /** * 流式对话 * @param content 输入的文本 * @param task 流式返回内容的回调函数 * @param end 返回内容结束的回调函数 */ public func chat(content:String, task!: (String) -> Unit = {o => print(o)},end!:()->Unit = {=>}){ robot.chat(content,task:task,taskEnd:end) } } 注’ 完整的代码请参考提供的代码文件桌宠渲染接下来,需要实现可视化的窗口渲染,赋予“椰椰”一副“身体”。动画播放窗口为了实现AI桌宠的渲染,首先需要有一个窗口。对此,笔者选择基于RGF_CJ中的WinBase类,实现一个自己的窗口类WinMain。// Yez/src/win_anm.cj // 使用 RGF_CJ 的宏实现代码简化 @LifeCycle // 自动管理 设备相关资源 的释放 @HookProc // 自动挂载窗口类的消息事件方法 class WinMain <: WinBase{ ... // 窗口类初始化时,传入 ResMgr 载入的资源(设备无关资源) init(anm:Array<(AnmInfo, MemBitmap)>){ _anmRes = anm } /** * 加载设备无关资源 -> 设备相关资源 * @note 本部分将资源ResMgr类中的图片资源,载入到窗口类中 */ private func loadAnm(anm:Array<(AnmInfo, MemBitmap)>){ surface .createBitmapFromMemory(_bmps[0],anm[0][1]) .createBitmapFromMemory(_bmps[1],anm[1][1]) .createBitmapFromMemory(_bmps[2],anm[2][1]) .createBitmapFromMemory(_bmps[3],anm[3][1]) } /** * 创建设备资源事件,窗口渲染表面创建完成后,开始载入资源 */ public func createDeviceResources():Bool{ // 这里调用 loadAnm 载入图片资源 loadAnm(_anmRes) return true; } /** * 窗口创建完成事件,当窗口创建完成时,开始渲染动画 */ public func created(hwnd:RgfHwnd,x:Int32, y:Int32, nWidth:Int32, nHeight:Int32):Unit{ ... } /** * 绘制窗口事件,此处根据当前的动画状态渲染窗口 */ public override func onPaint(wRect:Rect):Unit{ surface.clear(0.0,0.0,0.0,0.0) match(_nowState){ case Wait => {=> // 等待状态的动画渲染比较特殊,因为需要实现眼睛跟随鼠标移动 if(_bmps[3].isValid() && _bmps[0].isValid()){ // 这一部分计算当前渲染帧的静态部分,即:身体, // 并且将这部分内容渲染到窗口 .... // 然后下面计算出鼠标对眼睛造成的偏移量 // 并且将眼睛渲染到身体上 if(_anmStep < _anmRes[0][0].frames){ .... }else{ .... } } }() case Jump => {=> // 这里渲染椰椰的跳跃动画 if(_bmps[1].isValid()){ .... } }() case Speak => {=> // 这里渲染椰椰正在说话的动画 if(_bmps[2].isValid()){ .... } }() } } } 在入口函数中,启用UIMain并且创建窗口,代码如下:// Yez/src/win_anm.cj main(): Int64 { ... if(ResMgr.i.loadRes()){ // 开始创建UI UIMain{=> // 确保皮肤配置有效 if(!ResMgr.i.skip.contains(ConfigMgr.i.skip)){ ConfigMgr.i.skip = ResMgr.i.skip.keys().toArray()[0] } // 创建窗口类 let anmWin:WinMain = WinMain(ResMgr.i.skip.get(ConfigMgr.i.skip).getOrThrow()) // 先随便在桌面 (0,0) 的位置创建一个 10像素×10像素 的窗口 // 窗口类内部会在显示前根据配置中的信息自动布局到正确位置和尺寸 anmWin.createWin(uiGetContext(), "Yez", 0, 0, 10, 10, 0, WinStyle.e2n(WinStyle.Popup), // 使用的是 TopMost 分层窗口(支持透明背景) dwExStyle:WinStyleEx.Default | WinStyleEx.TopMost, // 渲染使用的是 Direct2D 计数 layered:true,substrate:Substrate.Direct2D ) } } ... } 完成上述代码后,窗口已经可以正确的渲染动画了注’ 完整的代码请参考提供的代码文件编辑框窗口对于编辑框的开发是比较繁琐的,因此选择直接继承RGF_UI的UIInput类,然后将此控件修改为窗口,首先需要创建一个编辑框窗口类InputWin,代码如下:// Yez/src/win_input.cj public open class InputWin <: UIInput{ // 为编辑框设置独立的UI样式,而不是继承全局的样式 let css:UITheme = UITheme() init(){ super(theme:css) // 从配置中获取字体大小 css.fontSizeM = ConfigMgr.i.fontSize // 更新样式配置 css.update() } /** * 获取输入框窗口预期出现的位置 * @return 编辑框窗口坐标 * @note 本部分计算编辑框在当前椰椰所在桌面中心时的位置 */ private func getTargetLocation():Point{ // 方案二、桌面中心 let monitor = rsGetMonitorFromPoint(rsGetCursorPos()) let pos = Point( ((monitor.right - monitor.left) - INPUT_WIN_WIDTH) / 2, ((monitor.bottom - monitor.top) - INPUT_WIN_HEIGHT) / 2 ) return pos } /** * 创建窗口 */ public func createWin(hwndParent:RgfHwnd):RgfHwnd{ // 获取出现位置坐标 let pos = getTargetLocation() // 创建一个置顶层的无边框编辑框窗口 let retv = super.createWin( uiGetContext(),"",pos.x,pos.y,INPUT_WIN_WIDTH,INPUT_WIN_HEIGHT,hwndParent, WinStyle.PopupWindow | WinStyle.ClipChildren, dwExStyle:WinStyleEx.Default | WinStyleEx.TopMost, substrate:Substrate.Direct2D,layered:false,bgMod:BackGroundMod.CoverNotDraw,drawInterval:0 ) // 设置默认的内容为:“想对椰椰说什么:” content = INPUT_PROMPT // 创建好就显示窗口 show(true) return retv } } 完成编辑框窗口的创建后,还需要能响应相关的输入事件。对此,笔者希望:a) 当编辑框被按下或输入内容时,如果内容为默认内容,则清空提示内容;b) 当编辑框失去焦点时,窗口主动销毁自身;c) 当编辑框按下回车键时,将编辑框内容上发给大模型,并开始处理返回消息。实现自动清空内容很简单,代码如下:// Yez/src/win_input.cj /** 鼠标左键按下时,判断内容并清空 */ public override open func onLButtonDown(e:EvMouse):Unit{ if(content == INPUT_PROMPT){ content = "" } super.onLButtonDown(e) } /** 键盘按下某键时,判断内容并清空 */ public override open func onKeyDown(e:EvKey):Unit{ if(content == INPUT_PROMPT){ content = "" } super.onKeyDown(e) } 得益于RGF_CJ库的安全性,窗口销毁会自动释放资源,避免了繁琐的资源释放,和释放时机的管理。实现窗口失去焦点就是否窗口的代码如下:// Yez/src/win_input.cj /** 窗口失去焦点时,销毁窗口并释放窗口资源 */ public override open func onKillFocus(e:EvKillFocus):Unit{ destroyWin() } 希望实现按下回车键触发对话,则需要在对应的事件中调用LLMImpl类的流式对话方法,代码如下:// Yez/src/win_input.cj /** 字符输入事件 */ public override open func onChar(e:EvChar):Unit{ // 判断输入的是否为回车键 if(e.char == 0x0D/*VK_RETURN*/){ // 是回车键就销毁本窗口 destroyWin() // 并且调用 开始回答函数 start() // 然后调用 LLMImpl 类的流式对话方法 // 并且传入 流的回调函数 以及 回答结束的函数 LLMImpl.i.chat(content,task:task,end:end) }else{ super.onChar(e) } } // 回复流 public var task:(String) -> Unit = {str=>} // 开始回答 public var start:() -> Unit = {=>} // 开始回答 public var end:() -> Unit = {=>} 编辑框控件的效果如下图所示:注’ 完整的代码请参考提供的代码文件反馈窗口完成上述内容后,输入与处理已经基本完备,接下来需要实现文本的显示,这一部分涉及到内容绘制,同样还是基于WinBase类进行窗口开发。创建一个反馈文本的窗口类 WinText,并且准备基本的资源代码如下:// Yez/src/win_text.cj @Abbr // 文本色彩值转Color对象宏 @LifeCycle @HookProc open class WinText <: WinBase{ // 内容纯色画刷 private let _brhCnt:SolidColorBrush // 背景纯色画刷 private let _brhbg:SolidColorBrush // 文本渲染核心 private let _textCore:TextCore // 文本格式 private let _textformat:TextFormat // 文本布局 private let _textLayout:TextLayout // 文本内容 private var _text:String = "" /** 创建窗口 */ public func createWin( x:Int32, y:Int32, nWidth:Int32, nHeight:Int32, hWndParent:RgfHwnd, substrate!:Substrate = Substrate.Direct2D ):RgfHwnd{ ... } /** 文本对象管理方法,根据配置和窗口尺寸生成文本渲染依赖的对象 */ private func createFontObj(){ let rect:Rect = surface.getRect() surface.createTextCore(_textCore) .createTextFormat(_textformat,ConfigMgr.i.fontSize) .createTextLayout(_textLayout,_text,_textformat,Float32(rect.right),Float32(rect.bottom)) } /** 创建设备/渲染相关资源事件 */ public override open func createDeviceResources():Bool{ // 创建 纯黑内容画刷 和 白色半透明背景画刷 surface.createSolidColorBrush(_brhCnt,"#000",1.0) .createSolidColorBrush(_brhbg,"#FFFA",1.0) // 创建文本相关对象 createFontObj() return true; } } 完成渲染事件,确保内容正确渲染// Yez/src/win_text.cj // 绘制窗口 public override open func onPaint(wRect:Rect):Unit{ surface.clear(0.0,0.0,0.0,0.0) // 透明模式 // 渲染背景圆角矩形 surface.fillRoundedRectangle(surface.getRect(),4.0,4.0,_brhbg) // 渲染大模型返回的文本内容 surface.drawTexts(_textCore,_brhCnt,_textLayout,originX:0.0,originY:0.0) } 完成显示文本方法。当设置文本时,若窗口类已经存在,就更新内部文本;若窗口不存在,就根据当前内容创建窗口并显示。// Yez/src/win_text.cj public func showText(str:String,p:RgfHwnd):Unit{ // 获取光标位置 let pos = rsGetCursorPos() // 获取光标所在屏幕矩形范围 let monitor = rsGetMonitorFromPoint(pos) // 如果窗口未创建,就创建窗口 if(!isCreated()){ createWin(pos.x,pos.y,100,100,p) } if(_textLayout.isValid()){ if(str != _text){ // 更新文本内容 _text = str; // 创建文本布局信息 surface.createTextLayout(_textLayout,_text,_textformat,Float32(monitor.right - monitor.left) / 2.0,Float32(monitor.bottom - monitor.top) / 2.0) } // 获取文本自动布局后的测量数据 let m = _textLayout.getMetrics() let w = Int32(m.getOrThrow().width) let h = Int32(m.getOrThrow().height) 按照测量数据移动窗口 if(pos.x > (monitor.right - monitor.left) / 2){ move(pos.x - w,pos.y - h,w,h) }else{ move(pos.x,pos.y - h,w,h) } } // 窗口显示 show(true) } 文本控件的效果如下图所示:注’ 完整的代码请参考提供的代码文件联结各个窗口逻辑完成上述各个控件后,需要实现各个组件之间的联合。实现流式信息与动画的联动// Yez/src/win_anm.cj /** 文本变更事件 */ func onTextChange(str:String):Unit{ if(!_firstText){ _firstText = true // 当文本输出开始,切换“跳跃”动画状态到“说话”动画状态 play(AnmState.Speak) } _text += str _textBox.showText(_text,hWnd) } /** 大模型开始输出文本 */ func onTextStart():Unit{ killTimer(0) _text = "" // 给大模型上发请求后,播放“跳跃”动画 play(AnmState.Jump) _firstText = false } /** 大模型结束输出文本 */ func onTextEnd():Unit{ // 大模型输出结束后,延迟5s,以让用户完整阅读完内容 const cj_bug_val:Option<CFunc<(HWND, UInt32, UInt64, UInt32) -> Unit>> = None setTimer(0,5000,cj_bug_val) } /** 时钟事件 */ func onTimer(e:EvTimer):Unit{ killTimer(0) // 用户阅读完内容后,销毁文本框窗口 _textBox.destroyWin() // 播放“等待”动画 play(AnmState.Wait) } 双击“椰椰”时打开输入框窗口// Yez/src/win_anm.cj // 客户区双击鼠标左键 func onLButtonDblClk(e:EvMouse):Unit{ // 在“等待”状态下双击“椰椰” match(_nowState){ case AnmState.Wait => {=> // 销毁文本框(虽然已无可能还显示,不过保险起见) _textBox.destroyWin() if(!_inputBox.isCreated()){ // 创建文本输入框窗口 _inputBox.createWin(hWnd) }else{ // 如果窗口存在就移动输入框窗口 // 这部分适用于希望输入框跟随鼠标位置的情况 // 对于方案二的桌面居中而言,没有必要 _inputBox.move() } () }() case _ => () } } 结语至此,仓颉AI桌宠“椰椰”就基本开发完成了,其实还有很多规划的内容可以增加,比如当和椰椰说“咔嚓”或者“拍照”时激活截图功能。这些内容就留给读者继续探索啦!本文相关链接本文项目代码库:https://gitcode.com/raozj/YezRGF_CJ库:https://gitcode.com/Cangjie-SIG/RGF_CJRGF_UI库:https://gitcode.com/raozj/RGF_UIRGF案例库:https://gitcode.com/raozj/RGF_CJ_Example华为云ModelArts:https://www.huaweicloud.com/product/modelarts/studio.html
-
【案例共创】基于仓颉编程语言+DeepSeek实现模拟课堂讨论助手项目概述本项目旨在通过华为开发者空间云主机,结合仓颉编程语言和DeepSeek API,开发一个简单的模拟课堂讨论助手。用户可以通过命令行输入相关知识点或问题,应用会返回该知识点的详细解释和记忆方法。这个助手可以用诙谐幽默的方式学习各种知识。适用对象喜欢用诙谐幽默的方式学习知识的爱好者。案例流程说明:① 使用CodeArts IDE for Cangjie编辑器开发仓颉程序调用;② 领取免费 DeepSeeK Tokens;③ 仓颉对接DeepSeek接口。开发步骤1. 环境准备注册并登录华为开发者空间:访问华为开发者空间官网,注册并登录账户。创建云主机实例:在华为开发者空间中创建一个新的云主机实例,选择默认的配置。使用开箱即用的云主机实例开发,点击 打开云主机 再点击打开远程桌面。图示即可看出是开箱即用的CodeArts IDE for Cangjie编辑器2. 项目初始化创建新项目:首先我们进入到开发者空间后,双击打开CodeArts IDE for Cangjie,在CodeArts IDE for Cangjie中创建一个新的项目,命名为“demo”(随便你怎么命名)。名称和位置可以自定义,产物类型选择executable。产物类型说明: executable,可执行文件;static,静态库,是一组预先编译好的目标文件的集合;dynamic,动态库,是一种在程序运行时才被加载到内存中的库文件,多个程序共享一个动态库副本,而不是像静态库那样每个程序都包含一份完整的副本。配置项目结构:设置项目的基本目录结构,包括源代码文件、配置文件等。创建项目后,打开src目录下main.cj文件,在预置代码中增加函数和函数调用代码(没有的话可以自己新建一份,代码内容下文我有附上)。然后点击右上的运行按钮运行项目,在终端中查看输出内容。如果遇到弹窗提示运行/调试配置,点击确定继续操作。修改后的main.cj文件代码:package demo func println_add(a: Int64, b:Int64): Int64 { let number: Int64 = a+b println(number) return number } main(): Int64 { println("hello world") println_add(3,4) println("hello Cangjie") return 0 } 调试项目:点击右上调试按钮,首次会提示配置调试,点击新增配置项,选择Cangjie(cjdb)Debug——launch。调试模式说明:Launch模式,启动调试器的同时加载被调程序;Attach,针对正在运行的程序,附加到已启动的程序。然后可以在main.cj代码中设置断点,再次点击调试按钮。可以在顶部看到调试按钮,在左侧看到调试过程中的变量、监视、调用堆栈、断点信息。2.1 版本管理 可选 (链接到gitcode)点完之后会要求你填入访问密钥先登录gitcode 然后点访问令牌 新建访问令牌 一直下一步即可 会得到一串令牌 然后将令牌输入即可。然后新建一个仓库 名字你自己取当前项目代码仓是空仓库你可以通过克隆仓库开始或使用以下方式为你的项目添加文件:或为你的项目添加以下文件: 添加 README.md添加 LICENSE添加 .gitignore命令行指引你还可以按照以下说明从你的电脑中上传现有文件或项目。Git 全局设置git config --global [user.name](http://user.name/) "weixin_41024010" git config --global user.email "[weixin_41024010@noreply.gitcode.com](mailto:weixin_41024010@noreply.gitcode.com)" 创建一个新仓库git clone [https://gitcode.com/weixin_41024010/test.git](https://gitcode.com/weixin_41024010/test.git) cd test echo "# test" >> [README.md](http://readme.md/) git add [README.md](http://readme.md/) git commit -m "add README" git branch -m main git push -u origin main推送现有的文件cd existing_folder git init git remote add origin [https://gitcode.com/weixin_41024010/test.git](https://gitcode.com/weixin_41024010/test.git) git add . git commit -m "Initial commit" git branch -m main git push -u origin main推送现有的 Git 仓库cd existing_repo git remote rename origin old-origin git remote add origin [https://gitcode.com/weixin_41024010/test.git](https://gitcode.com/weixin_41024010/test.git) git push -u origin --all git push -u origin --tags � 配置好git 就可以使用了 ,但是有个地方要先设置一下:提交文件限制改大一些 ,不然如果没有配置好GIT忽略文件的话 ,会推送不上去。改动可以从这里提交 ,这样就完成了代码的版本控制3. Cangjie对接DeepSeek接口华为云提供了单模型200万免费Tokens,包含DeepSeek-R1&V3满血版,我们可以登录华为云ModelArts Studio(MaaS)控制台领取免费额度,这里我们选择DeepSeek-R1满血版来搭建我们的工程。在云主机桌面底部菜单栏,点击打开火狐浏览器。用火狐浏览器访问ModelArts Studio首页:https://www.huaweicloud.com/product/modelarts/studio.html,点击ModelArts Studio控制台跳转到登录界面,按照登录界面提示登录,即可进入ModelArts Studio控制台。你也可以在本地浏览器打开进行相关操作。领取后点击调用说明,可以获取到对应的API地址、模型名称。当然API Key也要创建下,这里创建很简单,按提示跳转过去就成,我这里就不介绍了。记录对应的API地址、模型名称、API Key留作下面步骤使用。4. Cangjie对接DeepSeek编写代码进入云主机桌面,右键选择Open Terminal Here打开终端命令窗口,克隆仓颉示例代码仓库。git clone https://gitcode.com/CaseDeveloper/Cangjie-Examples.git 使用CodeArts IDE for Cangjie打开AIChat示例项目。左上点击文件—打开项目,选择前面克隆的示例代码目录Cangjie-Examples下的AIChat打开。修改配置config.json配置文件,配置DeepSeek的API信息。配置项:model、model、base_url{ "model": "DeepSeek-V3", "api_key": "Your KEY", "base_url": "[https://maas-cn-southwest-2.modelarts-maas.com/v1/infers/8a062fd4-7367-4ab4-a936-5eeb8c4/v1/chat/completions](https://maas-cn-southwest-2.modelarts-maas.com/v1/infers/8a062fd4-7367-4ab4-a936-5eeb8fb821c4/v1/chat/completions)", "system_prompt": "模拟课堂讨论 - Simulated Classroom Discussion,I need you to help me memorize the noun explanation, after I type a noun, you will simulate 5 students in the class to generate their speeches about the noun. The discussion must be humorous, and easy to understand. The entire conversation and instructions should be provided in Chinese. The first term is: 主题是" } 打开main.cj,运行项目,我们就可跟AI助手进行聊天了。(* 注意:打开main.cj可能提示Console标红,不影响程序运行,可以忽略。亦可以删除import std.console.Console重新编写该导入语句)至此,基于仓颉编程语言+DeepSeek实现模拟课堂讨论助手开发就完成了。接下来我们在聊天项目基础上,改编成“模拟课堂讨论助手”助手,有时候英文提示词会更有效其实超级简单,我们只需要再修改 “system_prompt” 即可。修改内容如下:"system_prompt": "模拟课堂讨论 - Simulated Classroom Discussion,I need you to help me memorize the noun explanation, after I type a noun, you will simulate 5 students in the class to generate their speeches about the noun. The discussion must be humorous, and easy to understand. The entire conversation and instructions should be provided in Chinese. The first term is: 主题是" 运行程序:最后我把代码完整的给大家呈现下(chat.cj):所有代码开源在git clone [https://gitcode.com/weixin_41024010/HUAWEIDEMO.git](https://gitcode.com/weixin_41024010/HUAWEIDEMO.git) 大家可以克隆我的代码使用。使用时要根目录是CHATDEEPSEEK (就是如图所示)不然编译会有问题。具体代码如下:package openai_chat import encoding.json.stream.* import net.http.ClientBuilder import net.http.HttpHeaders import net.http.HttpRequestBuilder import net.tls.TlsClientConfig import net.tls.CertificateVerifyMode import std.collection.ArrayList import std.io.ByteArrayStream import std.time.Duration public struct FunctionCall <: JsonDeserializable<FunctionCall> & JsonSerializable { public let name: String public let arguments: String public init(name: String, arguments: String) { this.name = name this.arguments = arguments } public static func fromJson(r: JsonReader): FunctionCall { var temp_name: String = "" var temp_arguments: String = "" while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "name" => temp_name = r.readValue<String>() case "arguments" => temp_arguments = r.readValue<String>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for FunctionCall") } } return FunctionCall(temp_name, temp_arguments) } public func toJson(w: JsonWriter) { w.startObject() w.writeName("name").writeValue<String>(this.name) w.writeName("arguments").writeValue<String>(this.arguments) w.endObject() w.flush() } } public enum RoleType { User | Assistant | System | Function | NULL } public func role_type_to_str(role: RoleType): Option<String> { return match(role) { case RoleType.User => Some("user") case RoleType.Assistant => Some("assistant") case RoleType.System => Some("system") case RoleType.Function => Some("function") case RoleType.NULL => None } } public func str_to_role_type(role_option_str: Option<String>): RoleType { return match(role_option_str) { case Some(role_option) => match (role_option) { case "user" => RoleType.User case "assistant" => RoleType.Assistant case "system" => RoleType.System case "function" => RoleType.Function case x => throw Exception("unknow enum ${x} for RoleType") } case None => RoleType.NULL } } public struct Message<: JsonDeserializable<Message> & JsonSerializable { public let role: RoleType public var content: String public let function_call: Option<FunctionCall> public init(role: RoleType, content: String) { this.role = role this.content = content this.function_call = None } public init(role: RoleType, content: String, function_call: Option<FunctionCall>) { this.role = role this.content = content this.function_call = function_call } public static func fromJson(r: JsonReader): Message { var temp_role: Option<String> = None // role may null in stream chat var temp_content: String = "" var temp_functional_call: Option<FunctionCall> = None while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "role" => temp_role = r.readValue<Option<String>>() case "content" => temp_content = r.readValue<String>() case "function_call" => temp_functional_call = r.readValue<Option<FunctionCall>>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for Message") } } let role_type: RoleType = str_to_role_type(temp_role) return Message(role_type, temp_content) } public func toJson(w: JsonWriter) { w.startObject() w.writeName("role").writeValue<Option<String>>(role_type_to_str(this.role)) w.writeName("content").writeValue<String>(this.content) w.endObject() w.flush() } } public struct ChatRequest <: JsonSerializable { private let model: String private let messages: ArrayList<Message> private let max_tokens: Int64 private let temperature: Float64 private let top_p: Float64 private let n: Int32 private let stream: Bool public init( model: String, messages: ArrayList<Message>, max_tokens: Int64, temperature: Float64, top_p: Float64, n: Int32, stream: Bool ) { // construction function with messages this.model = model this.messages = messages this.max_tokens = max_tokens this.temperature = temperature this.top_p = top_p this.n = n this.stream = stream } public init( model: String, prompt: String, history: ArrayList<(String, String)>, system_prompt: String, max_tokens: Int64, temperature: Float64, top_p: Float64, n: Int32, stream: Bool ){ // construction function with prompt and system_prompt this.model = model this.messages = ArrayList<Message>([ Message(RoleType.System, system_prompt) ]) for ((use_msg, bot_msg) in history) { this.messages.append(Message(RoleType.User, use_msg)) this.messages.append(Message(RoleType.Assistant, bot_msg)) } this.messages.append(Message(RoleType.User, prompt)) this.max_tokens = max_tokens this.temperature = temperature this.top_p = top_p this.n = n this.stream = stream } public init( model: String, prompt: String, history: ArrayList<(String, String)>, system_prompt: String, stream: Bool ){ // construction function with prompt and default arguments this.model = model this.messages = ArrayList<Message>([ Message(RoleType.System, system_prompt) ]) for ((use_msg, bot_msg) in history) { this.messages.append(Message(RoleType.User, use_msg)) this.messages.append(Message(RoleType.Assistant, bot_msg)) } this.messages.append(Message(RoleType.User, prompt)) this.max_tokens = 2000 this.temperature = 0.7 this.top_p = 1.0 this.n = 1 this.stream = stream } public func toJson(w: JsonWriter) { w.startObject() w.writeName("model").writeValue<String>(this.model) w.writeName("messages").writeValue<ArrayList<Message>>(this.messages) w.writeName("max_tokens").writeValue<Int64>(this.max_tokens) w.writeName("temperature").writeValue<Float64>(this.temperature) w.writeName("top_p").writeValue<Float64>(this.top_p) w.writeName("n").writeValue<Int32>(this.n) w.writeName("stream").writeValue<Bool>(this.stream) w.endObject() w.flush() } } public struct Choice <: JsonDeserializable<Choice> & JsonSerializable { public let index: Int32 public let message: Option<Message> public let delta: Option<Message> public let finish_reason: Option<String> public let logprobs: Option<Float64> // dashscope for qwen need public init( index: Int32, message: Option<Message>, delta: Option<Message>, finish_reason: Option<String>, logprobs: Option<Float64> ) { this.index = index this.message = message this.delta = delta this.finish_reason = finish_reason this.logprobs = logprobs } public static func fromJson(r: JsonReader): Choice { var temp_index: Int32 = -1 var temp_message: Option<Message> = None var temp_delta: Option<Message> = None var temp_finish_reason: Option<String> = None var temp_logprobs: Option<Float64> = None while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "index" => temp_index = r.readValue<Int32>() case "message" => temp_message = r.readValue<Option<Message>>() case "delta" => temp_delta = r.readValue<Option<Message>>() case "finish_reason" => temp_finish_reason = r.readValue<Option<String>>() case "logprobs" => temp_logprobs = r.readValue<Option<Float64>>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for Choice") } } return Choice(temp_index, temp_message, temp_delta, temp_finish_reason, temp_logprobs) } public func toJson(w: JsonWriter) { w.startObject() w.writeName("index").writeValue<Int32>(this.index) w.writeName("message").writeValue<Option<Message>>(this.message) w.writeName("delta").writeValue<Option<Message>>(this.delta) w.writeName("finish_reason").writeValue<Option<String>>(this.finish_reason) w.writeName("logprobs").writeValue<Option<Float64>>(this.logprobs) w.endObject() w.flush() } } public struct Usage <: JsonDeserializable<Usage> & JsonSerializable { public let prompt_tokens: UInt64 public let completion_tokens: UInt64 public let total_tokens: UInt64 public init(prompt_tokens: UInt64, completion_tokens: UInt64, total_tokens: UInt64) { this.prompt_tokens = prompt_tokens this.completion_tokens = completion_tokens this.total_tokens = total_tokens } public static func fromJson(r: JsonReader): Usage { var temp_prompt_tokens: UInt64 = 0 var temp_completion_tokens: UInt64 = 0 var temp_total_tokens: UInt64 = 0 while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "prompt_tokens" => temp_prompt_tokens = r.readValue<UInt64>() case "completion_tokens" => temp_completion_tokens = r.readValue<UInt64>() case "total_tokens" => temp_total_tokens = r.readValue<UInt64>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for Usage") } } return Usage(temp_prompt_tokens, temp_completion_tokens, temp_total_tokens) } public func toJson(w: JsonWriter) { w.startObject() w.writeName("prompt_tokens").writeValue<UInt64>(this.prompt_tokens) w.writeName("completion_tokens").writeValue<UInt64>(this.completion_tokens) w.writeName("total_tokens").writeValue<UInt64>(this.total_tokens) w.endObject() w.flush() } } public struct ChatResponse <: JsonDeserializable<ChatResponse> { // some api names `id`, and some names `request_id` public let id: Option<String> public let request_id: Option<String> public let system_fingerprint: Option<String> public let model: String public let object: String public let created: UInt64 public let choices: ArrayList<Choice> public let usage: Option<Usage> public init( id: Option<String>, request_id: Option<String>, system_fingerprint: Option<String>, model: String, object: String, created: UInt64, choices: ArrayList<Choice>, usage: Option<Usage> ) { this.id = id this.request_id = request_id this.system_fingerprint = system_fingerprint this.model = model this.object = object this.created = created this.choices = choices this.usage = usage } public static func fromJson(r: JsonReader): ChatResponse { var temp_id: Option<String> = None var temp_request_id: Option<String> = None var temp_system_fingerprint: Option<String> = None var temp_model: String = "" var temp_object: String = "" var temp_created: UInt64 = 0 var temp_choices: ArrayList<Choice> = ArrayList<Choice>([]) var temp_usage: Option<Usage> = None while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "id" => temp_id = r.readValue<Option<String>>() case "request_id" => temp_request_id = r.readValue<Option<String>>() case "system_fingerprint" => temp_system_fingerprint = r.readValue<Option<String>>() case "model" => temp_model = r.readValue<String>() case "object" => temp_object = r.readValue<String>() case "created" => temp_created = r.readValue<UInt64>() case "choices" => temp_choices = r.readValue<ArrayList<Choice>>() case "usage" => temp_usage = r.readValue<Option<Usage>>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for ChatResponse") } } return ChatResponse( temp_id, temp_request_id, temp_system_fingerprint, temp_model, temp_object, temp_created, temp_choices, temp_usage ) } } public func get_domain( url: String ): String { var temp_url = url if (temp_url.startsWith("https://")) { temp_url = temp_url["https://".size..] } else if (temp_url.startsWith("http://")) { temp_url = temp_url["http://".size..] } let domain: String = temp_url.split("?")[0].split("/")[0] return domain } public func build_http_client( prompt: String, env_info: EnvInfo, history: ArrayList<(String, String)>, stream!: Bool ){ // prepare input data var array_stream = ByteArrayStream() let json_writer = JsonWriter(array_stream) let chat_res = ChatRequest( env_info.model, prompt, history, env_info.system_prompt, stream ) chat_res.toJson(json_writer) let post_data: Array<UInt8> = array_stream.readToEnd() var headers: HttpHeaders = HttpHeaders() headers.add("Authorization", "Bearer ${env_info.api_key}") headers.add("Content-Type", "application/json") if (stream) { headers.add("Accept", "text/event-stream") } let request = HttpRequestBuilder() .url(env_info.base_url) .method("POST") .body(post_data) .readTimeout(Duration.second * 120) .addHeaders(headers) .build() let client = if (env_info.base_url.startsWith("https")) { var tls_client_config = TlsClientConfig() tls_client_config.verifyMode = CertificateVerifyMode.TrustAll tls_client_config.domain = get_domain(env_info.base_url) ClientBuilder() .tlsConfig(tls_client_config) .build() } else { ClientBuilder().build() } return (request, client) } public func chat( prompt: String, env_info: EnvInfo, history: ArrayList<(String, String)> ): Option<String> { let (request, client) = build_http_client( prompt, env_info, history, stream: false ) var result_message: Option<String> = None var res_text = "" try { // call api let response = client.send( request ) // read result (support max revice 100k data) let buffer = Array<Byte>(102400, item: 0) let length = response.body.read(buffer) res_text = String.fromUtf8(buffer[..length]) // println("res_text: ${res_text}") var input_stream = ByteArrayStream() input_stream.write(res_text.toArray()) // convert text to ChatResponse object let json_reader = JsonReader(input_stream) let res_object = ChatResponse.fromJson(json_reader) let choices: ArrayList<Choice> = res_object.choices if (choices.size > 0) { let message = choices[0].message.getOrThrow() // println("message: ${message.content}") result_message = Some(message.content) } else { println("can't found any response") } } catch (e: Exception) { println("ERROR: ${e.message}, reviced text is ${res_text}") } client.close() return result_message } public func stream_chat( prompt: String, env_info: EnvInfo, history: ArrayList<(String, String)> ): Option<String> { let (request, client) = build_http_client( prompt, env_info, history, stream: true ) var result_response: String = "" var temp_text2 = "" try { // call api let response = client.send( request ) // read result let buffer = Array<Byte>(10240, item: 0) var finish_reason: Option<String> = None while(finish_reason.isNone() && temp_text2 != "[DONE]") { let length = response.body.read(buffer) let res_text = String.fromUtf8(buffer[..length]) for (temp_text in res_text.split("\n")) { temp_text2 = if (temp_text.startsWith("data: ")) { temp_text["data: ".size..] } else { temp_text } if (temp_text2.size == 0) { continue } if (temp_text2 == "[DONE]") { break } // println("========================") // println("temp_text: ${temp_text2}") // println("========================") var input_stream = ByteArrayStream() input_stream.write(temp_text2.toArray()) // convert text to ChatResponse object let json_reader = JsonReader(input_stream) let res_object = ChatResponse.fromJson(json_reader) let choices: ArrayList<Choice> = res_object.choices if (choices.size > 0) { finish_reason = choices[0].finish_reason if (finish_reason.isNone()) { let delta = choices[0].delta.getOrThrow() print("${delta.content}", flush: true) result_response += delta.content } } else { println("can't found any response") } } } } catch (e: Exception) { println("ERROR: ${e.message}, reviced text is ${temp_text2}") } client.close() if (result_response.size > 0) { return Some(result_response) } else { return None } } 5. 运行和测试最后我们测试下DeepSeek,如下图:到此整个项目案例创建完成,可见是非常简单且实用的,希望能给大家带来帮助。我正在参加【案例共创】第4期 基于华为开发者空间+仓颉/DeepSeek/MCP完成应用构建开发实践 cid:link_1参考链接https://gitcode.com/weixin_41024010/HUAWEIDEMOhttps://gitcode.com/Cangjie/Cangjie-Examples
-
一、 购买弹性云服务器一台用于后续凤凰商城应用的部署二、搭建华为云CodeArts凤凰商城项目1、创建项目控制台-> 开发与运维-> 需求管理 CodeArts Req -> 立即使用-> 选择示例项目-> DevOps全流程示例项目”的模板-> 创建项目(需要开通CodeArts服务,如果创建项目提示权限不足,则需设置项目创建者) 2、添加项目组成员进入项目->设置->通用设置->成员管理->添加成员->项目角色->保存 3、新建需求进入项目-> 工作-> 规划-> 添加规划-> 思维导图规划添加Epic、Feature、Story、Task 4、管理迭代规划进入项目-> 工作-> 迭代-> + -> 名称、时间迭代-> 未规划工作项-> 将story规划至迭代中可以监控和追踪项目状态,可以看统计、也可以上传相关文档统一管理等 5、管理项目通知进入项目->设置->通知设置定制项目工作流程进入项目->设置->公共状态设置->添加状态 6、代码托管进入项目->代码->代码托管-> 选择代码仓在导航中找到html文件->编辑-> 修改后保存 7、代码检查进入项目->代码->代码检查->找到任务->更多->设置->规则集->选择语音->确定->开始检查查看检查结果切换至代码检查任务概览页面->则可以查看代码问题 8、构建应用配置SWR服务->组织管理->创建组织配置并执行构建任务进入项目->持续交付->编译构建->选任务->更多->编辑->参数设置->保存并执行(若构建失败,检查参数配置) 9、部署应用(1)创建主机集群进入项目->设置->通用设置->基础资源管理->新建主机集群->配置基本信息、目标主机(导入ECS->配置授信信息)(2)向部署主机中安装依赖工具进入项目->持续交付->部署->新建应用进入部署任务配置页面->环境管理->新建环境->导入主机切换部署步骤页面->所有步骤->如docker->添加->环境选择->保存并部署(3)配置并执行引用部署任务进入项目->持续交付->部署->选择应用->更多->编辑->环境管理->新建环境->导入主机切换部署步骤页面->选择部署来源->设置选择源类型、环境、构建任务切换至参数设置页签->根据容器镜像服务指令填写参数(4)验证部署结果访问部署ECS的公网IP 10、持续测试进入项目->测试->测试计划->新建测试->测试用例->手工测试->需求目录->新建测试用例 (迭代状态变为设计中)执行测试计划进入项目->工作->需求管理->迭代-> 某迭代->将story改为测试中测试->测试用例->某迭代->手工测试->需求目录->选择 -> 填写实际结果 11、配置流水线(1)创建流水线进入项目->持续交付->流水线->新建流水线->系统模板页面自动跳转至流水线详细配置页面,添加代码检查、构建、部署任务(2)运行流水线配置完以上内容,保存并运行(3)配置质量门禁该流水线任务详情页->更多->编辑“代码检查”阶段->准出条件->添加标准策略准出条件->保存并运行切换到流水线“运行历史”页签,可以看到该流水线的执行历史记录
-
产品名称:MDC 610软件版本:MDC 610 1.99.102-0000000 问题现象(问题描述):摄像头程序在编译机上编译通过但是在MDC上显示Segmentation fault故障后已采取的措施:/示例代码:如下这是.c文件#include <iostream>#include <vector>#include <cstdint>#include <cstring>#include "camera.h" // 声明 CameraInit、CameraWaitEvents、CameraGetData、CameraData#include "ascend_hal.h" // 声明全局 halMbufFreeusing mdc::camera::CameraInit;using mdc::camera::CameraWaitEvents;using mdc::camera::CameraGetData;using mdc::camera::CameraData;int main(int argc, char* argv[]){ // 默认参数 uint32_t camId = 21; int32_t timeoutWait = 1000; int32_t timeoutGet = 1000; // 简单命令行解析 for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--camera") == 0 && i+1 < argc) { camId = static_cast<uint32_t>(std::stoi(argv[++i])); } else if (std::strcmp(argv[i], "--wait-timeout") == 0 && i+1 < argc) { timeoutWait = std::stoi(argv[++i]); } else if (std::strcmp(argv[i], "--get-timeout") == 0 && i+1 < argc) { timeoutGet = std::stoi(argv[++i]); } else { std::cerr << "Usage: " << argv[0] << " [--camera ID] [--wait-timeout ms] [--get-timeout ms]\n"; return -1; } } // 1. 初始化摄像头 std::vector<uint32_t> initList = { camId }; int32_t ret = CameraInit(initList); if (ret != 0) { std::cerr << "CameraInit failed: " << ret << "\n"; return -1; } // 2. 等待事件 auto ready = CameraWaitEvents(timeoutWait); if (ready.empty()) { std::cerr << "No camera event\n"; return -1; } // 3. 获取数据 std::vector<CameraData> frames; ret = CameraGetData(camId, frames, timeoutGet); if (ret != 0 || frames.empty()) { std::cerr << "CameraGetData failed: " << ret << "\n"; return -1; } // 4. 打印第一帧信息 —— 替换下列字段为实际名称 CameraData &f = frames[0]; std::cout << "Camera ID: " << /* f.realCameraIdField */ 0 << "\n"; std::cout << "Timestamp: " << /* f.realTimestampField */ 0 << "\n"; std::cout << "Image size: " << /* f.realImgSizeField */ 0 << "\n"; std::cout << "Meta size: " << /* f.realMetaSizeField */ 0 << "\n"; // 5. (可选)解析 metaBuf // const uint8_t* meta = f.realMetaBufPtr; // TODO // size_t msize = f.realMetaSizeField; // TODO // for (size_t off = 0; off + 1 < msize; off += 2) { // uint16_t word = (uint16_t(meta[off]) << 8) | uint16_t(meta[off+1]); // std::cout << "0x" << std::hex << word << " "; // } // std::cout << std::dec << "\n"; // 6. 释放缓冲 for (auto &d : frames) { // TODO: 替换为实际 buf 指针成员 // ::halMbufFree(d.realImgBufPtr); // ::halMbufFree(d.realMetaBufPtr); } return 0;}这是CMakeLists.txtcmake_minimum_required(VERSION 3.10)project(camera_example CXX)# 读取交叉编译环境变量if(DEFINED ENV{CC}) set(CMAKE_C_COMPILER $ENV{CC})endif()if(DEFINED ENV{CXX}) set(CMAKE_CXX_COMPILER $ENV{CXX})endif()if(DEFINED ENV{SYSROOT}) set(CMAKE_SYSROOT $ENV{SYSROOT})endif()# 计算交叉编译器自带 include 路径get_filename_component(CLANG_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY)get_filename_component(CROSS_ROOT ${CLANG_BIN_DIR} DIRECTORY)set(CROSS_INCLUDE_DIR ${CROSS_ROOT}/include)# 通用编译/链接选项set(COMMON_FLAGS -O2 --target=aarch64-linux-gnu --sysroot=${CMAKE_SYSROOT})add_compile_options(${COMMON_FLAGS})add_link_options (${COMMON_FLAGS})# 生成可执行add_executable(camera_example camera_example.cpp)# 包含头文件路径target_include_directories(camera_example PRIVATE ${CROSS_INCLUDE_DIR} ${CMAKE_SYSROOT}/usr/include ${CMAKE_SYSROOT}/usr/include/driver)# 链接库路径target_link_directories(camera_example PRIVATE ${CMAKE_SYSROOT}/usr/lib64)# 链接 SDK 库(根据实际库名调整)find_library(LIB_CAMERA NAMES mdc_camera camera PATHS ${CMAKE_SYSROOT}/usr/lib64)find_library(LIB_MBUF NAMES mdc_mbuf PATHS ${CMAKE_SYSROOT}/usr/lib64)if(NOT LIB_CAMERA OR NOT LIB_MBUF) message(FATAL_ERROR "找不到 SDK 库 (libcamera.so / libmdc_camera.so 或 libmdc_mbuf.so)")endif()target_link_libraries(camera_example PRIVATE ${LIB_CAMERA} ${LIB_MBUF})这是Makefile文件# Makefile —— 读取环境变量:CC, CXX, SYSROOTSRC := camera_example.cppTARGET := camera_example# 交叉编译器根目录CROSS_ROOT := $(shell dirname $(shell dirname $(CXX)))CXXFLAGS := -O2 \ --target=aarch64-linux-gnu \ --sysroot=$(SYSROOT) \ -I$(SYSROOT)/usr/include \ -I$(SYSROOT)/usr/include/driver \ -I$(CROSS_ROOT)/includeLDFLAGS := --target=aarch64-linux-gnu \ --sysroot=$(SYSROOT) \ -L$(SYSROOT)/usr/lib64 \ -lmdc_camera \ -lmdc_mbufall: $(TARGET)$(TARGET): $(SRC)$(CXX) $(CXXFLAGS) $< -o $@ $(LDFLAGS)clean:rm -f $(TARGET).PHONY: all clean
yd_251998187
发表于2025-06-03 11:35:33
2025-06-03 11:35:33
最后回复
yd_251998187
2025-06-16 22:14:57
224 17 -
基于华为开发者空间使用 FastAPI 构建 MCP 天气查询服务案例介绍本案例将指导您在华为云开发者空间中使用 FastAPI 框架构建一个功能完整的 MCP(Model Context Protocol)天气查询服务。通过本案例,您将学会如何将传统的 REST API 转换为 MCP 工具,让 AI 模型能够实时获取天气信息并与用户进行智能交互。技术栈后端框架: FastAPI协议标准: MCP (Model Context Protocol)天气数据: 和风天气 API认证方式: JWT (JSON Web Token)客户端: Cherry Studio部署环境: 华为云开发者空间学习目标掌握 MCP 协议的基本概念和实现方式实现 AI 模型与外部服务的实时数据交互体验华为云开发者空间的开发便利性以及 Maas DeepSeek 模型服务的无缝集成案例内容1. 概述1.1 案例背景在传统的 AI 对话系统中,模型只能基于训练数据回答问题,无法获取实时信息。MCP 协议的出现解决了这一痛点,它允许 AI 模型通过标准化的接口调用外部工具和服务,获取实时数据。本案例选择天气查询作为示例,因为:实时性强: 天气数据需要实时获取,体现 MCP 的价值应用广泛: 天气查询是用户常见需求技术完整: 涵盖 API 调用、数据处理、错误处理等完整流程易于扩展: 可以基于此案例扩展更多功能1.2 适用对象企业用户:关注业务自动化、提升开发效率、集成AI能力,适合希望快速构建智能工具、提升企业数字化水平的团队。个人开发者:具备一定Python基础(或能借助 AI 辅助编程),关注技术创新与个人成长,适合希望掌握MCP协议、API开发与AI集成的开发者。高校学生:有编程基础(或能借助 AI 辅助编程),关注AI应用实践与创新能力培养,适合希望参与AI+云平台项目实训、提升综合开发能力的学生群体。1.3 案例时间准备阶段: 30 分钟(环境配置、账号注册)开发阶段: 20 分钟(代码编写、调试)测试阶段: 10 分钟(功能验证、MCP 集成)总计时间: 约 60 分钟1.4 案例流程环境准备API 注册项目初始化依赖安装核心代码开发JWT 认证实现天气接口开发MCP 集成本地测试客户端配置端到端验证1.5 资源总览本案例预计花费0元。资源类型名称/说明免费额度/价格备注开发资源华为云开发者空间0元免费注册使用Maas DeepSeek 模型服务0元免费额度,按需扩展和风天气开发者账号0元免费额度,1000次/天Cherry Studio 客户端0元开源免费API 配额和风天气免费版0元1000次/天Maas DeepSeek 模型服务0元免费额度注意事项:本案例基于免费额度,如需更高配额请自行申请。API 可能存在调用次数限制,超出免费额度需付费或申请扩容。2. 环境配置2.1 开发者空间配置步骤 1: 登录华为云领取免费云主机访问 华为云官网,注册并登录华为云账号点击“开发者->开发者空间”,进入。如果没有领取过云主机可以点击"立即体验"进行开通。步骤 2: 配置云主机访问开发者空间工作台, 点击"配置云主机"配置基础资源: CPU架构为 ARM,规格为 4 vCPUs 8 GB,操作系统为 Ubuntu ,镜像为 Ubuntu 24.04 Server定制版等待环境初始化完成后点击"打开云主机-进入桌面",打开云主机桌面2.2 MaaS 领取 DeepSeek步骤 1: 访问华为云 MaaS 服务在华为云控制台搜索"MaaS",点击进入 Maas 控制台区域选择西南-贵阳一,在左侧菜单栏,选择在线推理 > 预置服务 > 免费服务,选择DeepSeek-R1-32K模型,点击领取额度,领取200万免费token。步骤 2: 获取API地址和模型名称领取后点击调用说明*,可以获取到对应的API地址、模型名称。步骤 3: 获取 API 密钥点击“API Key管理-创建API Key“”,自定义标签和描述,点击确定创建API Key。(⚠️注意:API Key仅会在新建后显示一次,请及时复制并妥善保存,若API Key丢失,请新建API Key。)至此,我们获取到了所需的 API 地址、模型名称和 API 密钥。2.3 安装 Cherry Studio下载安装:使用云主机中的Firefox访问 [Cherry Studio 官网] https://cherry-studio.com根据操作系统下载对应版本,如 **Cherry-Studio-1.3.12-arm64.AppImage给下载的文件添加执行权限chmod +x Cherry-Studio-1.3.12-arm64.AppImage安装运行相关依赖sudo apt update sudo apt install zlib1g zlib1g-dev sudo add-apt-repository universe sudo apt install libfuse2运行安装包./Cherry-Studio-1.3.12-arm64.AppImage --no-sandbox基础配置:配置 DeepSeek 模型连接测试模型连接是否正常2.4 和风天气 API 注册(可选)💡 提示: 教程代码中已包含测试用的 API 配置,如无法使用请自行注册注册流程:访问 和风天气开发者平台 https://id.qweather.com/注册开发者账号并完成邮箱验证进入开发服务控制台创建项目并获取必要的认证信息获取配置信息:PROJECT_ID: 项目标识符KEY_ID: JWT 密钥标识PRIVATE_KEY: JWT 签名私钥WEATHER_API_HOST: API 服务地址访问[https://id.qweather.com/] https://id.qweather.com/ 注册账号,初次使用需绑定手机号和邮箱。注册成功后,点击菜单中的 开发服务控制台,进入开发控制台。点击项目管理-创建项目,创建一个项目,如 fastapi, 创建之后此步骤获得PROJECT_ID。点击项目如fastapi,进入项目详情页并点击创建凭据开始创建 JWT 凭据,需先在本地生成公私钥对再创建,创建之后此步骤获得PRIVATE_KEY和KEY_ID。(PS: 虽然 APIKey 更简单,但推荐使用JSON Web Token (JWT)的认证方式获得更高等级的安全性以及不受限的API请求)点击设置, 从开发者信息中获取 API Host,也就是WEATHER_API_HOST至此,我们天气查询 MCP 中需要用到的配置信息已经全部获取。3. 项目搭建3.1 项目初始化使用CodeArts IDE for Python创建项目工程或者使用脚本创建项目目录:mkdir qweather-mcp cd qweather-mcp安装依赖包:pip install fastapi uvicorn fastapi-mcp pyjwt cryptography requests -i https://repo.huaweicloud.com/repository/pypi/simple创建项目结构:qweather-mcp/ ├── main.py # 主程序文件 ├── requirements.txt # 依赖列表 ├── config.py # 配置文件(可选) └── README.md # 项目说明3.2 核心代码实现详细的代码实现包括:FastAPI 应用初始化JWT 认证机制天气 API 封装MCP 协议集成错误处理机制3.3 关键功能模块JWT 认证模块: 实现安全的 API 访问控制天气查询模块: 封装和风天气 API 调用MCP 集成模块: 将 REST API 转换为 MCP 工具数据模型模块: 定义请求和响应的数据结构3.4 代码实现我们创建 main.py 文件,并编写代码。3.4.1 引入依赖from fastapi import FastAPI, HTTPException, Depends, Query, status from fastapi.security import HTTPBearer from pydantic import BaseModel, Field from typing import List, Optional, Dict, Any import jwt import time import requests from datetime import datetime import base64 from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend # 引入 fastapi_mcp from fastapi_mcp import FastApiMCP, AuthConfig3.4.2 配置天气服务 APIapp = FastAPI(title="天气查询API") # 配置项 - 在实际应用中应从环境变量或配置文件中读取 # PRIVATE_KEY = """YOUR_PRIVATE_KEY""" # PROJECT_ID = "YOUR_PROJECT_ID" # KEY_ID = "YOUR_KEY_ID" # WEATHER_API_HOST = "your_api_host" PRIVATE_KEY = "MC4CAQAwBQYDK2VwBCIEIG2wMZga50X1YDHmR8jkE5TGKNXpriFPXXCT/kgwZkcT" PROJECT_ID = "3MKPRE78RN" KEY_ID = "T8B7JMU7AY" WEATHER_API_HOST = "ky6mte27bb.re.qweatherapi.com" 3.4.3 定义响应 Model# 响应模型 class Location(BaseModel): name: str id: str lat: str lon: str adm2: str adm1: str country: str tz: str utcOffset: str isDst: str type: str rank: str fxLink: str class LocationResponse(BaseModel): code: str location: List[Location] class WeatherNow(BaseModel): obsTime: str temp: str feelsLike: str icon: str text: str wind360: str windDir: str windScale: str windSpeed: str humidity: str precip: str pressure: str vis: str cloud: str dew: str class ReferSources(BaseModel): sources: List[str] license: List[str] class WeatherResponse(BaseModel): code: str updateTime: str fxLink: str now: WeatherNow refer: ReferSources class JWTRequest(BaseModel): expiry_seconds: Optional[int] = 900 # 默认15分钟 custom_claims: Optional[dict] = None # 允许添加自定义声明 class WeatherQuery(BaseModel): city: str = Field(..., description="城市名,如:北京") 3.4.4 JWT 令牌生成# 解码并加载私钥 try: _der_key_bytes = base64.b64decode(PRIVATE_KEY) PRIVATE_KEY = serialization.load_der_private_key( _der_key_bytes, password=None, backend=default_backend() ) except Exception as e: print(f"关键错误:无法从配置加载EdDSA私钥。错误: {e}") PRIVATE_KEY = None if PRIVATE_KEY is None: raise RuntimeError(f"关键错误:EdDSA私钥加载失败,应用无法启动。错误: {e}") # 令牌缓存(简单实现,生产环境应使用Redis等缓存系统) token_cache = { "token": None, "expires_at": 0 } # Bearer Token 安全头 token_auth_scheme = HTTPBearer() EXPECTED_BEARER_TOKEN = "huaweicloud" # 定义期望的固定Token # Bearer Token 验证依赖 async def verify_bearer_token(token_payload = Depends(token_auth_scheme)): if token_payload.credentials != EXPECTED_BEARER_TOKEN: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭据", headers={"WWW-Authenticate": "Bearer"}, ) return token_payload.credentials # 生成JWT令牌 def generate_jwt(expiry_seconds: int = 900): current_time = int(time.time()) # 构建标准JWT载荷 payload = { 'iat': current_time - 30, # 颁发时间(提前30秒,避免时钟偏差问题) 'exp': current_time + expiry_seconds, # 过期时间 'sub': PROJECT_ID # 主题(项目ID) } # JWT头部 headers = { 'kid': KEY_ID # 密钥ID } if PRIVATE_KEY is None: raise ValueError("JWT生成失败: 私钥未初始化或加载失败。") try: # 生成JWT encoded_jwt = jwt.encode(payload, PRIVATE_KEY, algorithm='EdDSA', headers=headers) # 更新缓存 token_cache["token"] = encoded_jwt token_cache["expires_at"] = current_time + expiry_seconds - 60 # 提前1分钟过期,确保安全 return encoded_jwt except Exception as e: raise ValueError(f"JWT生成失败: {str(e)}") # 获取有效的JWT令牌(如果缓存中有有效令牌则使用缓存,否则生成新令牌) def get_valid_token(): current_time = int(time.time()) # 检查缓存中的令牌是否有效 if token_cache["token"] and token_cache["expires_at"] > current_time: return token_cache["token"] # 生成新令牌 return generate_jwt() @app.post("/generate-jwt", operation_id="generate_jwt", tags=["JWT"]) async def create_jwt( request: JWTRequest = JWTRequest() ): """ 生成JWT令牌 - 使用EdDSA算法签名 - 默认有效期为15分钟 - 可以添加自定义声明 """ try: encoded_jwt = generate_jwt(request.expiry_seconds) current_time = int(time.time()) return { "jwt": encoded_jwt, "expires_at": datetime.fromtimestamp(current_time + request.expiry_seconds).isoformat(), "issued_at": datetime.fromtimestamp(current_time - 30).isoformat(), "valid_for_seconds": request.expiry_seconds } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) 3.4.5 天气查询接口# 发送HTTP请求到天气API,并处理gzip压缩 def fetch_weather_api(endpoint: str, params: Dict[str, Any]): # 获取有效的JWT令牌 token = get_valid_token() headers = { 'Authorization': f'Bearer {token}', 'Accept-Encoding': 'gzip' # 我们请求gzip压缩 } url = f"https://{WEATHER_API_HOST}/{endpoint}" try: response = requests.get(url, headers=headers, params=params) # 检查响应状态 if response.status_code != 200: # 如果是401或403,尝试刷新令牌并重试 if response.status_code in [401, 403]: # 强制生成新令牌 new_token = generate_jwt() # 更新请求头 headers['Authorization'] = f'Bearer {new_token}' # 重试请求 response = requests.get(url, headers=headers, params=params) # 如果还是失败,则抛出异常 if response.status_code != 200: raise HTTPException( status_code=response.status_code, detail=f"天气API请求失败: HTTP {response.status_code}" ) else: raise HTTPException( status_code=response.status_code, detail=f"天气API请求失败: HTTP {response.status_code}" ) # 依赖 requests 库自动处理Gzip解压缩,并直接解析JSON # 旧的Gzip处理逻辑已被移除 return response.json() except requests.exceptions.JSONDecodeError as e: # 如果响应不是有效的JSON(即使在解压缩后),则捕获此特定错误 raise HTTPException(status_code=500, detail=f"天气API响应解析失败: 无效的JSON内容 - {str(e)}") except HTTPException: # 重新抛出已捕获的HTTPException,以便FastAPI处理 raise except Exception as e: # 捕获其他潜在错误 raise HTTPException(status_code=500, detail=f"天气API请求时发生未知错误: {str(e)}") @app.get("/city/lookup", response_model=LocationResponse, operation_id="lookup_city", tags=["天气查询"]) async def lookup_city( location: str = Query(..., description="城市名称,如:北京") ): """ 根据城市名称查询位置ID - 返回城市的详细信息和位置ID - 位置ID用于后续天气查询 """ try: data = fetch_weather_api("geo/v2/city/lookup", {"location": location}) return data except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/weather/now", response_model=WeatherResponse, operation_id="get_weather_now", tags=["天气查询"]) async def get_weather_now( location: str = Query(..., description="位置ID,如:101010100") ): """ 获取指定位置的实时天气 - 需要提供位置ID - 返回当前天气详情 """ try: data = fetch_weather_api("v7/weather/now", {"location": location}) return data except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/weather/by-city", response_model=WeatherResponse, operation_id="get_weather_by_city", tags=["天气查询"]) async def get_weather_by_city( query: WeatherQuery ): """ 一站式查询城市天气 - 只需提供城市名 - 自动查询位置ID并获取天气 """ try: # 先查询城市ID location_data = fetch_weather_api("geo/v2/city/lookup", {"location": query.city}) # 检查是否找到城市 if location_data.get("code") != "200" or not location_data.get("location"): raise HTTPException(status_code=404, detail=f"找不到城市: {query.city}") # 获取第一个匹配城市的ID location_id = location_data["location"][0]["id"] # 查询天气 weather_data = fetch_weather_api("v7/weather/now", {"location": location_id}) return weather_data except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) 3.4.6 主程序及 MCP 集成@app.get("/") async def root(): """主页 - 提供API简介""" return { "message": "天气查询API", "endpoints": [ "/generate-jwt - 生成JWT令牌", "/city/lookup - 根据城市名查询位置ID", "/weather/now - 根据位置ID查询当前天气", "/weather/by-city - 一站式查询城市天气" ], "docs": "/docs 查看完整API文档" } # mcp 实现 mcp = FastApiMCP( app, name="My Weather MCP", description="天气查询API", include_operations=["get_weather_by_city"], # 只公开 get_weather_by_city 接口作为 MCP tool auth_config=AuthConfig(dependencies=[Depends(verify_bearer_token)]) # MCP 使用 Bearer Token 验证 ) mcp.mount() # 启动服务器的命令(在命令行中运行): # uvicorn main:app --reload if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8009) 4. 测试验证4.1 本地服务测试启动服务:python main.pyAPI 文档验证:访问 http://localhost:8009/docs测试各个接口功能验证数据格式正确性我们可以使用浏览器访问 http://localhost:8009/docs 来查看并测试API。4.2 MCP 客户端集成Cherry Studio 配置:服务名称: FastAPI 天气查询连接类型: 服务器发送事件(SSE)服务地址: http://localhost:8009/mcp认证头: Authorization=Bearer huaweicloud功能验证:配置 MCP 服务连接验证工具列表显示测试天气查询功能对比启用前后的效果在 Cherry Studio 中配置好的 MCP 服务,我们可以直接在 MCP 客户端中使用 get_weather_by_city 接口:名称:FastAPI 天气查询类型:服务器发送事件(SSE)URL:http://localhost:8009/mcp请求头:Authorization=Bearer zishu.co在工具中,我们可以看到get_weather_by_city接口已经添加到MCP服务中。新建一个话题,先不开启 MCP 服务器设置,直接输入北京天气怎么样,我们会发现大模型无法实时获取天气。接着我们启用 MCP 服务,并输入北京天气怎么样,我们会发现大模型可以实时获取天气。get_weather_by_city tool 被成功调用,并返回了天气信息。5. 总结5.1 技术收获通过本案例,您将掌握:MCP 协议: 理解 AI 模型与外部工具的交互机制FastAPI 开发: 快速构建高性能 Web APIJWT 认证: 实现安全的 API 访问控制第三方 API 集成: 学会封装和调用外部服务错误处理: 构建健壮的服务应用5.2 应用价值实时数据获取: AI 模型可以获取最新的天气信息用户体验提升: 提供准确、及时的天气查询服务技术架构: 为更复杂的 MCP 应用奠定基础可扩展性: 可以轻松添加更多天气相关功能如果想要拓展更多功能,您可以动手试试看!
-
本案例选择MPI应用来验证展示鲲鹏DevKit的多点并行调试能力以及鲲鹏平台远程编译调试能力。并借助开发者空间云主机提供的CodeArts IDE和鲲鹏服务器进行MPI应用的编译与并行调试,让开发者直观体验鲲鹏DevKit编译调试功能。1 概述1.1 背景介绍鲲鹏DevKit是覆盖软件迁移、应用开发和性能调优的全流程鲲鹏原生开发工具,可以帮助开发者加速应用迁移和算力升级,提供鲲鹏开发套件,包括应用迁移和性能分析等一些列软件工具。通过实际操作,让大家了解如何通过鲲鹏DevKit插件远程连接DevKit工具,并对应用进行远程编译调试,体验鲲鹏DevKit在鲲鹏原生开发过程中给开发者提供的便利。1.2 适用对象企业个人开发者高校学生1.3 案例时间本案例总时长预计120分钟。1.4 案例流程说明:① 自动部署鲲鹏服务器;② 安装鲲鹏DevKit插件;③ 下载MPI程序源文件;④ 环境安装;⑤ 编译应用;⑥ 并行调试。1.5 资源总览本案例预计花费总计0元。资源名称规格单价(元)时长(分钟)云主机2vCPUs4GBX86鲲鹏服务器2vCPUs8GBEulerOS 2.82 操作步骤2.1 自动部署鲲鹏服务器在云主机桌面右键选择“Open Terminal Here”,打开命令终端窗口。执行自动部署命令如下:hcd deploy --password abcd1234@ --time 7200 命令的参数说明:password:password关键字后设置的是鲲鹏服务器的root用户密码,命令中给出的默认为abcd1234@,开发者可以替换成自定义密码(至少8个字符)。time:time关键字后面设置的为鲲鹏服务器的可用时间,单位为秒,至少600秒。本案例建议申请的时间为120分钟,即7200秒。记录部署鲲鹏服务器公网IP,例如截图中对应的就是:1.94.112.100,实际操作时使用个人部署的鲲鹏服务器IP。2.2 安装鲲鹏DevKit插件由于CodeArts IDE在线安装的鲲鹏DevKit工具版本过低,所以需要前往鲲鹏社区官网下载最新鲲鹏DevKit插件到云主机,下载地址为:插件市场。如下图所示,进入到下载页面后选择最新版本进行下载。如果插件市场无法下载或下载失败可直接在浏览器下载。在云主机桌面,打开CodeArts IDE for Python/Java开发环境,单击右侧“扩展”按钮,点击“从本地安装”,选择下载好的插件安装。在弹窗中选择“继续安装”。2.3 下载MPI程序源文件在云主机桌面右键选择“Open Terminal Here”,打开命令终端窗口,输入以下命令下载并解压待使用的MPI程序源文件。wget https://codeload.github.com/kunpengcompute/devkitdemo/zip/refs/heads/devkitdemo-23.0.1 unzip devkitdemo-23.0.1 解压后“Compiler_and_Debugger/mpi_demo/”路径下的bcast_demo.c作为MPI程序源文件。2.4 环境安装以下工具或依赖库安装更新都是在2.1自动部署的鲲鹏服务器上进行的,不是云主机,确定好操作环境再进行安装。使用“ssh root@鲲鹏服务器公网IP”命令连接远程鲲鹏服务器,“鲲鹏服务器公网”就是自动部署鲲鹏服务器后记录的IP地址。2.4.1 安装mpicc由于在自动部署拉起的服务器上进行编译调试时,需要使用mpicc,mpicc是MPI实现的一部分,常见的MPI实现有OpenMPI和MPICH。这里安装OpenMPI。使用ssh命令(ssh root@鲲鹏服务器公网IP)连接远程鲲鹏服务器后,执行以下命令安装OpenMPI。sudo yum update -y sudo yum install -y openmpi openmpi-devel配置环境变量:1.编辑配置文件,进入Vim窗口后,按“i”编辑profile。sudo vim /etc/profile2.在文件末尾添加以下内容。export PATH=\$PATH:/usr/lib64/openmpi/bin3.按“ECS”退出编辑,再按“:wq”保存修改并退出,然后刷新配置文件。source /etc/profile安装 OpenMPI 后会提供 mpicc,查看 mpicc 的版本信息成功,表明 mpicc 已经成功安装在你的系统中。mpicc --version2.4.2 安装MPI由于目前环境中mpirun版本为2.1.1,不满足lldb-server服务要求,需要更新。这里我们安装MPI 4.1.4版本。同样在远程鲲鹏服务器中,下载安装包。mkdir -p /path/to/OpenMPI cd /path/to/OpenMPI wget https://dtse-mirrors.obs.cn-north-4.myhuaweicloud.com/case/0018/openmpi-4.1.4.tar.gz tar -zxvf openmpi-4.1.4.tar.gz安装依赖包。yum install -y numactl-devel-\* systemd-devel-\* 加载编译器。export PATH=/path/to/GUN/bin:\$PATH export LD_LIBRARY_PATH=/path/to/GUN/lib64:\$LD_LIBRARY_PATH 执行以下命令进行配置预编译。cd openmpi-4.1.4 ./configure --prefix=/path/to/OPENMPI --enable-pretty-print-stacktrace --enable-orterun-prefix-by-default --with-cma --enable-mpi1-compatibility CC=gcc CXX=g++ FC=gfortran安装编译。make -j 2 make install配置环境变量:1.编辑配置文件,进入Vim窗口后,按“i”编辑profilesudo vim /etc/profile2.在文件末尾添加export PATH=/path/to/GNU/bin:/path/to/OPENMPI/bin:\$PATH export LD_LIBRARY_PATH=/path/to/GNU/lib64:/path/to/OPENMPI/lib:\$LD_LIBRARY_PATH 3.按“ECS”退出编辑,再按“:wq”保存修改并退出,然后刷新配置文件。source /etc/profile/path/to/OPENMPI/:OpenMPI实际安装的路径,请根据实际情况进行替换。执行以下命令验证OpenMPI是否安装成功。mpirun --version2.4.3 创建libstdc++.so.6软链接由于/usr/lib64/libstdc++.so.6版本较旧,不包含GLIBCXX_3.4.26,不满足lldb-server需求。这里用软链接方式将/usr/lib64/libstdc++.so.6 的链接指向 /opt/Devkit/tools/libstdc++.so.6。建议备份/usr/lib64/libstdc++.so.6。sudo mv /usr/lib64/libstdc++.so.6 /usr/lib64/libstdc++.so.6.bak建立软链接。sudo ln -s /opt/DevKit/tools/libstdc++.so.6 /usr/lib64/libstdc++.so.6 检查软链接。ls -l /usr/lib64/libstdc++.so.6 2.5 编译应用在云主机桌面双击打开CodeArts IDE,打开解压后的MPI程序,即解压后“Compiler_and_Debugger/mpi_demo/”路径。打开工程后,点击打开bcast_demo.c文件,修改85行为:“int color = rankNum / 1;”。即将调试时划分通信子组规则由2个rank为1个通信子组改为1个rank为1个通信子组。进入鲲鹏DevKit插件,选择使用鲲鹏DevKit窗口中的“开始使用”,配置IP和端口,IP是2.1自动部署鲲鹏服务器得到的弹性公网IP,端口是8086,服务证书选择“信任当前服务证书”。首次登录需要创建管理员密码,登录成功后,点击“编译”,添加目标服务器。添加远程鲲鹏服务器为目标服务器,服务器IP地址为鲲鹏服务器弹性公网IP地址。然后再次点击“编译”到编译配置窗口,服务器信息会自动带出,配置编译命令后再点击“编译”。编译命令如下:mpicc -g bcast_demo.c -o bcast_demo首次创建后再修改编译和调试配置可以通过点击CodeArts IDE底部“鲲鹏DevKit”,点击右上角“设置”按钮,再选择 “工程设置”即可对编译和调试配置进行二次修改。根据提示输入鲲鹏服务器密码,编译成功会在鲲鹏服务器/root/workspace/mpi_demo目录下生成带调试信息的可执行文件bcast_demo。2.6 并行调试在DevKit插件中点击“调试”,打开调试页面,选择“HPC并行应用”,配置MPI应用调试参数,点击“开始调试”。应用程序:/root/workspace/mpi_demo/bcast_demo应用程序源码路径:/root/workspace/mpi_demoMPI运行命令行:mpirun --allow-run-as-root -np 2参数说明如下:参数说明远程服务器配置进行HPC并行应用调试的目标服务器Linux用户名输入启动MPI应用的Linux用户名称Linux用户密码使用的Linux用户密码SSH端口驶入启动MPI应用的服务器SSH端口号应用程序输入的MPI应用,支持动态检索并显示应用程序路径应用程序参数(可选)传递给应用程序运行的参数应用程序源码路径源码和MPI应用存放的共享路径,支持动态检索并显示应用程序源码路径。环境变量设置(可选)输入运行HPC并行应用所需要的环境变量调试启动方式调试启动方式可选: mpirun命令运行方式 多瑙调度器运行方式 Slurm调度器运行方式MPI运行命令行输入的mpirun命令以及对应的命令参数,rank数目为1~2048OpenMP应用(可选)勾选后,需要输入OpenMP线程数。死锁检测(可选)勾选后,需要输入死锁超时时间。rank:rank 用于标识 MPI 并行程序中各个进程的唯一编号,在特定通信组内具有唯一性,且决定进程间通信和数据交互的顺序与方式。在 MPI 并行程序执行时,会启动多个进程协同工作,rank 就像是每个进程的 “身份证号”。开始调试后会读取rank状态:在rank状态读取过程中,若rank状态读取全部成功,会自动跳转到MPI应用调试页面。页面上获取到运行和调试区、源码区和调试功能区,运行和调试区域包括调试信息区和rank信息区。HPC并行应用调试支持三种调试粒度,分别为“全部”调试、“rank”调试或“通信组”调试。调用方式效果全部在RANK信息区域选择“全部”方式进行调试,选择某一个rank,对其进行调试会应用到全部rank。rank在RANK信息区域选择“rank”方式进行调试,对单一rank进行调试。通信组在RANK信息区域选择“通信组”方式进行调试,选择通信组中的某一个rank,对其进行调试会应用到整个通信组。调试按钮操作描述:操作操作描述继续点击执行到下一个断点单步跳过点击执行到下一行单步调试点击步入函数单步跳出点击步出函数停止点击后停止调试RANK信息区域选择“全部”调试方式,在89行代码处、47行代码处和93行代码处打上断点,再继续执行后续操作。选择“全部”调试方式,单击“继续”按钮,代码执行到89行,再单击“下一步”执行MPI_Comm_split(MPI_COMM_WORLD, color, rankNum, &row_comm)函数,该函数可将所有的rank进行通信分组。这里将2个rank生成2个通信子组,rank1在通信子组1,rank0在通信子组2。生成2个通信子组后,所有rank的源码执行到90行代码处,选择“通信组”调试方式,选择通信子组1中的rank1,单击“下一步”按钮,通信子组1中的rank代码执行到92行,通信子组2中的rank0代码无变化,停留在90行。调试通信子组1:未调试通信子组2:选择“通信组”调试方式,在左侧通信子组1中选择rank1,单击“继续”按钮,代码执行到47行,单击“下一步”按钮,执行MPI_Barrier(MPI_COMM_WORLD)函数,函数执行完成后,通信子组1会一直处于等待状态,此时,在左侧通信子组2中选择rank0,单击“继续”按钮,代码执行到47行,单击“下一步”按钮,执行MPI_Barrier(MPI_COMM_WORLD)函数,函数执行完成后,通信子组1不再等待,同步执行到49行代码处。rank1处于等待状态:rank0执行后,rank1、rank同步执行到49行:单击“点击放大”按钮,在CodeArts IDE面板中能看到通信子组的变化概览,每100ms显示通信子组的变化。通信子组的变化用不同颜色的菱形来区分,蓝色表示通信子组创建,紫色表示通信子组清除,黄色表示100ms内存在通信子组创建和通信子组清除。释放掉47行断点,RANK信息区域选择“全部”调试方式,循环点击“继续”按钮,代码执行到93行,在单击“下一步”按钮,执行MPI_Comm_free(&row_comm)函数,当不再调试某一通信子组时,可以释放创建的通信子组,该函数能实现释放创建的通信子组。点击侧边或下方工具栏中“运行和调试”查看调试信息。至此,使用鲲鹏DevKit工具进行MPI应用并行调试完成。3 问题记录3.1 编译调试问题1、重复操作案例或更换鲲鹏服务器重新编译失败报错:cd: /root/workspace/mpi_demo: No such file or directory原因:mpi_demo在第一次编译时同步项目到鲲鹏服务器中,并在项目下缓存了记录,想要再次编译时触发同步项目需要删除mpi_demo下的缓存文件。解决:删除云主机中mpi_demo项目下的缓存文件./devkit,删除后重新点击编译,参考2.5编译应用配置编译命令(mpicc -g bcast_demo.c -o bcast_demo)即可。cd /home/developer/Desktop/devkitdemo-devkitdemo-23.0.1/Compiler_and_Debugger/mpi_demo rm -rf .devkit/ “/home/developer/Desktop/devkitdemo-devkitdemo-23.0.1/Compiler_and_Debugger/mpi_demo”是云主机本地mpi_demo路径,以个人操作时实际路径为准。
-
1 概述1.1 背景介绍鲲鹏数学库(KML, Kunpeng Math Library)是基于鲲鹏平台优化的高性能数学函数库,由多个子库组成,广泛应用于科学计算、HPC等领域。 通过本案例,用户可以快速掌握鲲鹏数学库的安装与使用,并对所涉及的数学库性能表现有清晰认识。适用对象1.2 适用对象企业个人开发者高校学生1.3 案例时间本案例总时长预计30分钟。1.4 案例流程说明:① 自动部署并连接鲲鹏服务器;② 安装数学库;③ 修改环境变量;④ 数学库性能测试。1.5 资源总览本次实验预计花费总计0元。资源名称规格单价(元)时长(h)云主机2vCPUs4GB RAM免费0.52 操作步骤2.1 自动部署鲲鹏服务器本实验中,使用云主机提供的鲲鹏沙箱资源,只需要执行简单的自动部署命令即可拉起一台免费的鲲鹏服务器。在云主机桌面右键选择“Open Terminal Here”,打开终端命令窗口。输入自动部署命令,命令如下:hcd deploy --password abcd1234@ --time 1800命令的参数说明:password:password关键字后设置的是鲲鹏服务器的root用户密码,命令中给出的默认为abcd1234@,开发者可以替换成自定义密码(至少8个字符)。time:time关键字后面设置的为鲲鹏服务器的可用时间,单位为秒,至少600秒。当前实验在命令中申请的时间为30分钟,即1800秒。记录下自动部署后生成的鲲鹏服务器公网IP地址。2.2 安装数学库使用命令登录到鲲鹏服务器,命令如下:ssh root@鲲鹏服务器公网IP输入密码,密码为步骤2.1中自动部署命令行中“–password”后面的参数,命令中给出的默认为abcd1234@,如果没有修改,就使用abcd1234@进行登录,如果设置了自定义密码,直接输入自定义的密码(注意:输入过程中密码不会显示,密码输入完成按回车键结束)。使用命令下载数学库软件包,本次案例使用的是1.7.0版本。wget https://repo.oepkgs.net/openeuler/rpm/openEuler-20.03-LTS-SP3/extras/aarch64/Packages/b/boostkit-kml-1.7.0-1.aarch64.rpm解压软件包。rpm2cpio boostkit-kml-1.7.0-1.aarch64.rpm | cpio -div添加软链接,使用ln -s命令创建软链接,类似于Windows中的快捷方式,它是一个特殊的文件,其内容是指向另一个文件或者目录的路径,当访问软链接时,系统会根据软链接中的路径找到实际指向的目标文件或目录来进行操作。下面三组命令分别创建了libkspblas.so、libkvml.so和libkm.so三个软链接,分别指向根据find命令找到对应实际的kspblas.so、kvml.so和km.so文件cp -R /root/usr/local/kml/ /usr/local/cd /usr/local/kmlln -s 'find ./ -name \*kspblas.so\* -type f' ./libkspblas.soln -s 'find ./ -name \*kvml.so\* -type f' ./libkvml.soln -s 'find ./ -name \*km.so\* -type f' ./libkm.so2.3 修改环境变量在 Linux 系统中,LD_LIBRARY_PATH是一个重要的环境变量,它用于指定动态链接库(.so文件)的搜索路径。当程序在运行时需要加载动态链接库,系统会首先在默认的系统库路径中查找,然后会按照LD_LIBRARY_PATH环境变量所指定的路径顺序进行查找。将多个与/usr/local/kml相关的库目录添加到LD_LIBRARY_PATH环境变量中,是因为程序依赖于这些目录下的动态链接库来正确运行。通过将这些目录添加到LD_LIBRARY_PATH,可以确保程序在运行时能够找到所需的库文件。echo 'export LD\_LIBRARY\_PATH=/usr/local/kml/lib/kblas/locking:$LD\_LIBRARY\_PATH' >> /etc/profileecho 'export LD\_LIBRARY\_PATH=/usr/local/kml/lib:$LD\_LIBRARY\_PATH' >> /etc/profileecho 'export LD\_LIBRARY\_PATH=/usr/local/kml/lib/kblas/nolocking:$LD\_LIBRARY\_PATH' >> /etc/profileecho 'export LD\_LIBRARY\_PATH=/usr/local/kml/lib/kblas/omp:$LD\_LIBRARY\_PATH' >> /etc/profileecho 'export LD\_LIBRARY\_PATH=/usr/local/kml/lib/kblas/pthread:$LD\_LIBRARY\_PATH' >> /etc/profileecho 'export LD\_LIBRARY\_PATH=/usr/local/kml/lib/kvml/multi:$LD\_LIBRARY\_PATH' >> /etc/profileecho 'export LD\_LIBRARY\_PATH=/usr/local/kml/lib/kvml/single:$LD\_LIBRARY\_PATH' >> /etc/profileecho 'export LD\_LIBRARY\_PATH=/usr/local/kml/lib/kspblas/multi:$LD\_LIBRARY\_PATH' >> /etc/profileecho 'export LD\_LIBRARY\_PATH=/usr/local/kml/lib/kspblas/single:$LD\_LIBRARY\_PATH' >> /etc/profile执行命令使环境生效。source /etc/profile检查环境变量。env | grep LD\_LIBRARY\_PATH2.4 数学库性能测试1. KML_VML测试矢量数学库(Vector Math Library)借助计算密集型核心数学函数(幂函数、三角函数、指数函数、双曲函数、对数函数等)的矢量实施显著提升应用速度。使用命令创建测试文件test_sin.c文件。cdvi test\_sin.c进入到vim编辑器界面后,按下“i”键后,复制以下代码粘贴到编辑器中,复制完成后按下“ESC”键输入“:wq”,退出编辑器界面,该段代码主要功能是:初始化长度为100000的向量src,分别用计时器对循环使用系统函数库的sin函数求解以及调用KML_VML提供的向量三角函数vdsin求解,记录两种方法的耗时,对比KML_VML与系统函数库的性能。#include <stdio.h>#include <sys/time.h>#include <math.h>#include "kvml.h"#define LEN 100000int main(){ double src\[LEN\] = {0}; double dst1\[LEN\] = {0}; double dst2\[LEN\] = {0}; for(int i = 0; i < LEN; i++){ src\[i\] = i; } struct timeval start, end; long t;gettimeofday(&start, NULL); for(int i = 0; i < LEN; i++){ dst1\[i\] = sin(src\[i\]); }gettimeofday(&end, NULL); t = 100000 \* (end.tv\_sec - start.tv\_sec) + end.tv\_usec - start.tv\_usec; printf("Calculate Time without KML\_VML: %ld us \\n", t);gettimeofday(&start, NULL); vdsin(LEN, src, dst2);gettimeofday(&end, NULL); t = 100000 \* (end.tv\_sec - start.tv\_sec) + end.tv\_usec - start.tv\_usec; printf("Calculate Time with KML\_VML: %ld us \\n", t); return 0;}编译文件, 编译时添加动态库和头文件所在路径,并链接系统数学库和KML_VML动态库。gcc test\_sin.c -o test -L/usr/local/kml/lib/kvml/single -lkvml -lm -I/usr/local/kml/include -fopenmp -std=c99使用ldd指令检查程序依赖库是否准确链接。ldd test执行可执行文件,进行性能对比。./test结果显示,对于一个长度为100000的数组,用C语言的for循环实现求正弦函数值,需要6666微秒,而使用KML_VML仅需要779微秒,性能提升8倍。2. KML_BLAS测试BLAS(Basic Linear Algebra Subprograms)提供了一系列基本线性代数运算函数的标准接口,包括矢量线性组合、矩阵乘以矢量、矩阵乘以矩阵等功能。BLAS已被广泛的应用于工业界和科学计算,成为业界标准。KML_BLAS库提供BLAS函数的C语言接口。使用命令创建测试文件test_gemv.c文件。vi test_gemv.c进入到vim编辑器界面后,按下“i”键后,复制以下代码粘贴到编辑器中,复制完成后按下“ESC”键输入“:wq”,退出编辑器界面,该段代码主要功能是:初始化规模为1000*300的矩阵A,长度为300的向量x,长度为1000的向量y1和y2,分别用计时器对按照矩阵-向量的成家规则实现算法求解,即y=alpha*A*x+beta*y,以及调用KML_BLAS提供的函数cblas_dgemv求解,记录两种方法的耗时,对比KML_BLAS与手动实现矩阵乘加的性能。#include <stdio.h>#include <stdlib.h>#include <time.h>#include <sys/time.h>#include "kblas.h"#define M 1000#define N 300int main() { double alpha = 1.0; double beta = 1.0; double (\*A)\[N\] = (double (\*)\[N\])malloc(M \* N \* sizeof(double)); double \*x = (double \*)malloc(N \* sizeof(double)); double \*y1 = (double \*)malloc(M \* sizeof(double)); double \*y2 = (double \*)malloc(M \* sizeof(double)); srand((unsigned int)time(NULL)); for (int i = 0; i < M; i++) { for (int j = 0; j < N; j++) { A\[i\]\[j\] = (double)rand() / RAND\_MAX; } } for (int i = 0; i < N; i++) { x\[i\] = (double)rand() / RAND\_MAX; } for (int i = 0; i < M; i++) { y1\[i\] = (double)rand() / RAND\_MAX; y2\[i\] = (double)rand() / RAND\_MAX; } // 方法一:按照矩阵-向量的乘加规则实现算法求解 struct timeval start, end; long t;gettimeofday(&start, NULL);for (int i = 0; i < M; i++) { double sum = 0.0; for (int j = 0; j < N; j++) { sum += A\[i\]\[j\] \* x\[j\]; } y1\[i\] = alpha \* sum + beta \* y1\[i\];}gettimeofday(&end, NULL); t = 100000 \* (end.tv\_sec - start.tv\_sec) + end.tv\_usec - start.tv\_usec; printf("Calculate Time without KML\_BLAS: %ld us \\n", t); // 方法二:调用KML\_BLAS提供的函数cblas\_dgemv求解gettimeofday(&start, NULL); cblas\_dgemv(CblasRowMajor, CblasNoTrans, M, N, alpha, (const double \*)A, N, x, 1, beta, y2, 1);gettimeofday(&end, NULL); t = 100000 \* (end.tv\_sec - start.tv\_sec) + end.tv\_usec - start.tv\_usec; printf("Calculate Time with KML\_BLAS: %ld us \\n", t); return 0;}编译文件,编译时添加动态库和头文件所在路径,并链接KML_BLAS动态库。gcc test\_gemv.c -g -o test2 -L /usr/local/kml/lib/kblas/locking/ -lkblas -I /usr/local/kml/include -std=c99使用ldd指令检查程序依赖库是否准确链接。ldd test2执行可执行文件,进行性能对比。./test2结果显示,计算一个1000*300的矩阵-向量乘加运算,用C语言的for循环实现,需要1904微秒,而使用KML_BLAS仅需要145微秒,性能提升13倍。至此,本次案例体验全部完成。
-
西北赛区从整点变成20了 每次都慢5分钟 是不是代码写的有问题(bushi
-
华为云软件开发生产线CodeArts是一站式、全流程、安全可信的云原生DevSecOps平台,覆盖需求、开发、测试、部署、运维等软件交付全生命周期环节,为开发者打造全云化研发体验。华为云CodeArts目前已上线28款研发工具,服务于300多万开发者,应用于10多个行业,覆盖开发全场景。2025年1月,CodeArts发布了架构建模、CodeArts API相关新特性,具体内容如下: 架构建模 CodeArts Modeling新特性支持UML时间图支持UML类生成Java代码体验优化模型批注状态增加颜色进行区分问题修复修复无法查询元素在关联模型工程中创建关系的问题修复点击面包屑中项目名称跳转到需求管理首页的问题CodeArts API新特性支持上海一regionAPI设计模块支持导出html格式接口文档体验优化项目设置模块导入数据列表支持搜索和排序目前,华为云CodeArts已携手百万级开发者,在政府、物流、金融、教育、制造等10多个行业落地,助力企业构建了敏捷、高效、安全的数字化生产模式。使用华为云CodeArts,中国海油构建了供应链一体化数字化平台,研发工时节省了30%,智能油田管理系统集成、调试、部署时间从1周缩短为1天。中国邮政储蓄银行通过使用华为云CodeArts盘古助手打造智能开发平台,代码生成采纳率超30%,单元测试代码采纳率超60%,已自动生成29万余行高质量代码,高效支持超过200个应用系统的开发,实现了更好的智能化开发体验。未来,华为云CodeArts将积极增强软件开发的全流程智能协同,不断实现智能化创新,提升全流程研发效能,为开发者创造更多的业务价值,促进中国软件生态的繁荣。体验通道:https://www.huaweicloud.com/devcloud/?utm_source=developer.huaweicloud&utm_content=feature
-
写入完成后可以调整已写入内容的位置吗 会不会对导致读取和删除操作出现错误
-
请问本次比赛程序运行总时间是多少
-
【算法挑战营二十一期】为啥测评两天了不出结果?
-
整体log日志内容如下,请问这是什么情况,该怎么解决:2025-01-03 13:40:08【info】usecase0 build start2025-01-03 13:40:10【info】usecase0 build end2025-01-03 13:40:10【info】usecase0 round0 search start【E3:Run Error】选手程序执行中断
推荐直播
-
Skill 构建 × 智能创作:基于华为云码道的 AI 内容生产提效方案2026/03/25 周三 19:00-20:00
余伟,华为云软件研发工程师/万邵业(万少),华为云HCDE开发者专家
本次直播带来两大实战:华为云码道 Skill-Creator 手把手搭建专属知识库 Skill;如何用码道提效 OpenClaw 小说文本,打造从大纲到成稿的 AI 原创小说全链路。技术干货 + OPC创作思路,一次讲透!
回顾中 -
码道新技能,AI 新生产力——从自动视频生成到开源项目解析2026/04/08 周三 19:00-21:00
童得力-华为云开发者生态运营总监/何文强-无人机企业AI提效负责人
本次华为云码道 Skill 实战活动,聚焦两大 AI 开发场景:通过实战教学,带你打造 AI 编程自动生成视频 Skill,并实现对 GitHub 热门开源项目的智能知识抽取,手把手掌握 Skill 开发全流程,用 AI 提升研发效率与内容生产力。
回顾中 -
华为云码道:零代码股票智能决策平台全功能实战2026/04/18 周六 10:00-12:00
秦拳德-中软国际教育卓越研究院研究员、华为云金牌讲师、云原生技术专家
利用Tushare接口获取实时行情数据,采用Transformer算法进行时序预测与涨跌分析,并集成DeepSeek API提供智能解读。同时,项目深度结合华为云CodeArts(码道)的代码智能体能力,实现代码一键推送至云端代码仓库,建立起高效、可协作的团队开发新范式。开发者可快速上手,从零打造功能完整的个股筛选、智能分析与风险管控产品。
回顾中
热门标签