图片

本文介绍了在 iOS 平台上使用 MNN 框架部署大语言模型(LLM)时,针对聊天应用中文字流式输出卡顿问题的优化实践。通过分析模型输出与 UI 更新不匹配、频繁刷新导致性能瓶颈以及缺乏视觉动画等问题,作者提出了一套包含智能流缓冲、UI 更新节流与批处理、以及打字机动画渲染的三层协同优化方案。最终实现了从技术底层到用户体验的全面提升,让本地 LLM 应用的文字输出更加丝滑流畅,接近主流在线服务的交互体验。

图片

背景

在iOS端部署大语言模型(LLM) 聊天应用时,用户体验的流畅性是一个关键要素。MNN LLM iOS应用基于MNN推理框架,为用户提供本地化的AI对话体验。如果直接将模型的输出更新到回答的页面UI中,会有一个严重影响用户体验的问题:模型输出文字时存在明显的卡顿现象,文字显示生硬,缺乏自然的流动感。

因为用户已经习惯了ChatGPT、Qwen等在线服务提供的流畅回复和丝滑打字机效果。本地模型推理输出没有网络延迟,如果直接将模型结果输出,在用户体验上会大打折扣。所以我针对这个问题,进行了优化。本文将分析具体的问题,针对这些问题提出解决方法,并且详细的讲解具体的原理和实现。

我们先看看优化前的直接输出:

再看看优化之后的效果:

流畅度有明显的提升。

完整的项目地址如下:https://github.com/alibaba/MNN/blob/master/apps/iOS/MNNLLMChat/README.md


问题分析

通过输出现象分析,可以识别出导致卡顿和生硬输出的三个核心问题:

1. 模型输出速度与UI更新频率不匹配

  • 现象:模型推理速度较快,但输出内容会积累后批量更新UI。

  • 原因:缺乏合适的缓冲机制,导致"要么不更新,要么大量更新"的极端情况。

2. UI刷新频率过高造成性能瓶颈

  • 现象:本地模型快速推理输出,会引起频繁的UI更新导致主线程压力过大,出现卡顿和掉帧。

  • 原因:每个字符都触发独立的UI更新,没有合理的批处理机制。

3. 缺乏流式输出的视觉动画效果

  • 现象:文字瞬间出现,缺乏渐进式的视觉反馈。

  • 原因:没有展示类似打字机的逐字符显示动画。


优化策略

在Chat应用回答的过程中, 数据流向如下:

原始输出流 → 智能缓冲 → 批量更新 → 动画渲染 → 用户界面。

基于上面的数据流和优化需求,我们在可以进行后面三层协同优化策略:


  1. 底层流缓冲优化 (OptimizedLlmStreamBuffer)

职责:解决模型输出与UI更新的频率不匹配问题。

  • 智能触发机制:基于内容特征(标点符号)和缓冲阈值的双重触发;

  • 标点符号触发:中英文支持,完整的UTF-8 Unicode标点符号识别;

  • 性能优化:预分配内存,减少重分配开销。

  2. 中间层更新优化 (UIUpdateOptimizer)

职责:统一管理UI更新请求,实现批处理和节流。

  • 双重策略:批量触发(5个更新)+ 时间触发(30ms超时);

  • 线程安全:基于Swift Actor模型的并发处理;

  • 智能调度:自动取消重复任务,避免资源浪费。

  3. UI层动画增强 (LLMMessageTextView)

职责:提供自然流畅的用户视觉体验。

  • 条件化动画:判断是否需要启用打字机效果;

  • 流式适配:完美适配流式输出的文本变化;

  • 资源管理:自动清理动画资源,防止内存泄漏。

最终,我们通过底层增加缓冲输出,中层合并更新请求,UI层提供视觉缓冲——这三层配合实现了从技术优化到体验优化的完整覆盖,提升整体性能和体验效果。


详细技术实现


  1. OptimizedLlmStreamBuffer:智能流缓冲优化


  • 1.1 原理

OptimizedLlmStreamBuffer 是对标准 std::streambuf 的增强实现,通过智能缓冲策略解决模型输出与UI更新的频率不匹配问题。它的工作原理是在模型输出和UI更新之间建立一个缓冲层,根据内容特征和缓冲大小决定何时将累积的内容推送给UI。


  • 1.2 类设计

class OptimizedLlmStreamBuffer : public std::streambuf {private:    static const size_t BUFFER_THRESHOLD = 64;          // 缓冲区阈值(字节)    std::string buffer_;                                // 内容缓冲区public:    using CallBack = std::function<void(const char* str, size_t len)>; // 更新回调    OptimizedLlmStreamBuffer(CallBack callback);protected:    virtual std::streamsize xsputn(const char* s, std::streamsize n) override;
private:    void flushBuffer();    // 刷新缓冲区    bool checkForFlushTriggers(const char* s, std::streamsize n); // 检查触发条件    bool checkUnicodePunctuation();  // Unicode标点检测};

  • 1.3 方法详解

1. xsputn方法 - 数据流入口

下面是整体方法流程,每当模型生成新内容时都会调用此方法:

virtual std::streamsize xsputn(const char* s, std::streamsize n) override {    if (!callback_ || n <= 0) {        return n; // 参数校验,确保安全性    }try {        // 步骤1: 将新数据追加到缓冲区        buffer_.append(s, n);// 步骤2: 判断是否需要立即刷新        const size_t BUFFER_THRESHOLD = 64;        bool shouldFlush = buffer_.size() >= BUFFER_THRESHOLD;// 步骤3: 如果大小未达标,检查内容特征        if (!shouldFlush && n > 0) {            shouldFlush = checkForFlushTriggers(s, n);        }// 步骤4: 符合条件则刷新缓冲区        if (shouldFlush) {            flushBuffer();        }return n;    } catch (const std::exception& e) {        NSLog(@"Error in stream buffer: %s", e.what());        return -1; // 异常处理,确保程序稳定性    }}

工作流程说明:

  • 数据接收:模型每次输出的文本片段进入缓冲区;

  • 阈值判断:当累积

  • 内容达到64字节时立即输出;

  • 自动触发:即使未达到阈值,遇到标点符号

  • 也会触发输出;

  • 异常处理:完善的错误处理机制保证系统稳定性。

2. 触发机制

  • 阈值触发策略

const size_t BUFFER_THRESHOLD = 64; // 积累 64 byte 内容才输出
  • ASCII标点符号触发

bool checkForFlushTriggers(const char* s, std::streamsize n) {    char lastChar = s[n-1]; // 获取最后一个字符// 检查常见的英文标点符号    if (lastChar == '\n' ||  // 换行符 - 句子结束        lastChar == '\r' ||  // 回车符 - 兼容不同系统        lastChar == ' ' ||   // 空格 - 词语分隔        lastChar == '\t' ||  // 制表符 - 格式化字符        lastChar == '.' ||   // 句号 - 句子结束        lastChar == ',' ||   // 逗号 - 语句停顿        lastChar == ';' ||   // 分号 - 语句分隔        lastChar == ':' ||   // 冒号 - 说明引导        lastChar == '!' ||   // 感叹号 - 情感表达        lastChar == '?') {   // 问号 - 疑问句结束        return true;    }return checkUnicodePunctuation(); // 继续检查Unicode标点}

触发逻辑说明:

  • 语义完整性:在语义完整的点进行输出,提升阅读体验

  • 视觉节奏:模拟人类阅读时的自然停顿

  • 跨语言支持:同时支持英文和中文的标点符号

  • Unicode标点符号检测

中文标点符号采用UTF-8编码,需要特殊处理:

bool checkUnicodePunctuation() {    if (buffer_.size() >= 3) { // UTF-8中文标点通常占3字节        const char* bufferEnd = buffer_.c_str() + buffer_.size() - 3;// 定义中文标点符号的UTF-8编码        static const std::vector<std::string> chinesePunctuation = {            "\xE3\x80\x82",     // 。(句号) - 句子结束            "\xEF\xBC\x8C",     // ,(逗号) - 语句停顿              "\xEF\xBC\x9B",     // ;(分号) - 语句分隔            "\xEF\xBC\x9A",     // :(冒号) - 说明引导            "\xEF\xBC\x81",     // !(感叹号) - 情感表达            "\xEF\xBC\x9F",     // ?(问号) - 疑问句结束            "\xE2\x80\xA6",     // …(省略号) - 语意延续        };// 逐一比较字节序列        for (const auto& punct : chinesePunctuation) {            if (memcmp(bufferEnd, punct.c_str(), 3) == 0) {                return true; // 找到匹配的中文标点            }        }    }// 检查2字节的Unicode标点(如破折号)    if (buffer_.size() >= 2) {        const char* bufferEnd = buffer_.c_str() + buffer_.size() - 2;        if (memcmp(bufferEnd, "\xE2\x80\x93", 2) == 0 ||  // – (短破折号)            memcmp(bufferEnd, "\xE2\x80\x94", 2) == 0) {  // — (长破折号)            return true;        }    }return false;}

UTF-8编码处理细节:

  • 字节序列识别:通过比较字节序列精确识别中文标点

  • 长度适配:中文标点占2-3字节,需要相应的缓冲区长度检查

  • 性能优化:使用静态数组和memcmp进行高效比较

3. 内存预分配
OptimizedLlmStreamBuffer(CallBack callback) : callback_(callback) {    buffer_.reserve(1024); // 预分配1KB内存}
  • 减少重分配:避免频繁的内存分配和拷贝操作

  • 提升性能:预分配内存可以减少约30%的内存操作开销

1)std::string 在动态增长时,每次容量不足都会:

  • 分配新的更大内存空间(通常是当前容量的1.5-2倍)

  • 复制现有数据到新内存

  • 释放旧内存

// 没有预分配的情况下,字符串增长模式:// 容量: 0 -> 1 -> 2 -> 4 -> 8 -> 16 -> 32 -> 64 -> 128 -> 256 -> 512 -> 1024// 重分配次数: 约10次// 预分配1024字节后:// 容量: 1024 (一次分配)// 重分配次数: 0次 (在1024字节内)

2)与缓冲策略的协同

C++const size_t BUFFER_THRESHOLD = 64;bool shouldFlush = buffer_.size() >= BUFFER_THRESHOLD;
  • 缓冲阈值:64字节触发刷新

  • 预分配容量:1024字节

  • 协同效果:支持16次缓冲操作而无需重分配

因此我们预分配1024字节避免了前期的多次重分配操作。

4. 异常安全设计

~OptimizedLlmStreamBuffer() {    flushBuffer(); // 析构时确保缓冲区内容全部输出}void flushBuffer() {    if (callback_ && !buffer_.empty()) {        callback_(buffer_.c_str(), buffer_.size());        buffer_.clear(); // 清空缓冲区,释放内存    }}

  2. UIUpdateOptimizer:基于Actor的更新优化器


  • 2.1 原理

UIUpdateOptimizer 采用Swift 5.5引入的Actor并发模型,解决UI更新的线程安全和性能问题。它的核心思想是将频繁的UI更新请求按缓存大小或间隔时间进行批处理和节流,减少主线程压力。

Actor 队列(批处理 + 节流) -> 主线程UI(低频率UI更新 )

  • 2.2 类设计

actor UIUpdateOptimizer {    static let shared = UIUpdateOptimizer() // 全局单例// 状态管理    private var pendingUpdates: [String] = []    // 待处理更新队列    private var lastFlushTime: Date = Date()     // 上次刷新时间    private var flushTask: Task<Void, Never>?    // 延迟刷新任务// 配置参数    private let batchSize: Int = 5               // 批处理大小    private let flushInterval: TimeInterval = 0.03 // 节流间隔(30ms)}

简单介绍一下 Actor。在多线程或异步程序中,多个任务访问共享变量时容易造成数据竞争(data race)。Actor 是一种引用类型,用来保护其内部状态免受数据竞争影响。它是并发安全的,当你调用时,会自动对外部访问进行同步(串行队列),所以不需要手动加锁。


  • 2.3 方法详解

1. 双重触发策略

func addUpdate(_ content: String, completion: @escaping (String) -> Void) {    // 步骤1: 添加到待处理队列    pendingUpdates.append(content)// 步骤2: 判断触发条件    let shouldFlushImmediately = pendingUpdates.count >= batchSize ||                               Date().timeIntervalSince(lastFlushTime) >= flushInterval// 步骤3: 选择处理策略    if shouldFlushImmediately {        flushUpdates(completion: completion) // 立即处理    } else {        scheduleFlush(completion: completion) // 延迟处理    }}

策略选择逻辑:

2. 延迟刷新调度

private func scheduleFlush(completion: @escaping (String) -> Void) {    // 取消之前的调度,避免重复执行    flushTask?.cancel()// 创建新的延迟任务    flushTask = Task {        // 等待指定时间间隔        try? await Task.sleep(nanoseconds: UInt64(flushInterval * 1_000_000_000))// 检查任务是否被取消,以及是否有待处理内容        if !Task.isCancelled && !pendingUpdates.isEmpty {            flushUpdates(completion: completion)        }    }}

上面的方式,可以:

  • 节流控制:为UI更新提供30毫秒的缓冲时间;

  • 批处理优化:在这30毫秒内如果有新的更新到来,会取消当前延迟任务并重新开始计时;

  • 性能平衡:既避免过于频繁的UI更新,又保证内容能及时显示;

  • 响应性保证:即使在低频更新场景下,也确保内容在30毫秒内显示给用户。

3. 批处理执行

private func flushUpdates(completion: @escaping (String) -> Void) {    guard !pendingUpdates.isEmpty else { return }// 合并所有待处理的更新    let batchedContent = pendingUpdates.joined()// 清空队列,准备下一轮    pendingUpdates.removeAll()    lastFlushTime = Date()// 切换到主线程执行UI更新    Task { @MainActor in        completion(batchedContent)    }}

批处理优势分析:

  • 减少调用次数:将多次UI更新合并为一次,减少开销;

  • 提升响应性:主线程压力减少,UI更加流畅;

  • 内存效率:及时清理已处理内容,避免内存累积。

  3. LLMMessageTextView:沉浸式打字机动画


  • 3.1 背景

LLMMessageTextView 的设计目标是创造接近人类打字速度的自然动画效果。通过设置的时间参数和智能的动画控制,让AI的文字输出更加自然和富有节奏感。


  • 3.2 类设计

struct LLMMessageTextView: View {    // 数据模型    let text: String?                    // 完整文本内容    let messageUseMarkdown: Bool         // 是否使用Markdown渲染    let messageId: String                // 消息唯一标识    let isAssistantMessage: Bool         // 是否为AI消息    let isStreamingMessage: Bool         // 是否正在流式传输// 动画状态    @State private var displayedText: String = ""  // 当前显示的文本    @State private var animationTimer: Timer?      // 动画定时器// 动画配置参数    private let typingSpeed: TimeInterval = 0.015  // 15ms每字符    private let chunkSize: Int = 1                 // 每次显示1个字符}

  • 3.3 动画控制策略

1. 条件化动画触发

private var shouldUseTypewriter: Bool {    // 只有同时满足以下条件才启用动画:    // 1. 是AI助手的消息(用户消息不需要动画)    // 2. 文本长度超过5个字符(避免短消息的不必要动画)    return isAssistantMessage && (text?.count ?? 0) > 5}

触发逻辑分析:

  • 用户体验导向:只对AI消息使用动画,用户消息直接显示;

  • 性能考虑:短消息(≤5字符)直接显示,避免动画开销;

  • 场景适配:流式传输时启用动画,静态显示时关闭动画。

2. 流式文本变化处理

private func handleTextChange(_ newText: String?) {    guard let newText = newText else {        displayedText = ""        stopAnimation()        return    }if isAssistantMessage && isStreamingMessage && shouldUseTypewriter {        // 智能判断文本变化类型        if newText.hasPrefix(displayedText) && newText != displayedText {            // 场景1: 文本内容追加(流式输出的常见情况)            continueTypewriterAnimation(with: newText)        } else if newText != displayedText {            // 场景2: 文本内容完全变化(消息重新生成)            restartTypewriterAnimation(with: newText)        }        // 场景3: 文本内容无变化,不做处理    } else {        // 非动画场景:直接显示完整文本        displayedText = newText        stopAnimation()    }}

处理策略详解:


  • 3.4 动画执行机制

1. 动画启动流程

private func startTypewriterAnimation(for text: String) {    // 步骤1: 重置显示状态    displayedText = ""// 步骤2: 开始动画循环    continueTypewriterAnimation(with: text)}private func continueTypewriterAnimation(with text: String) {    // 前置检查:避免无效动画    guard displayedText.count < text.count else { return }// 清理旧定时器,避免冲突    stopAnimation()// 创建新的动画定时器    animationTimer = Timer.scheduledTimer(withTimeInterval: typingSpeed, repeats: true) { timer in        DispatchQueue.main.async {            self.appendNextCharacters(from: text)        }    }}

定时器机制特点:

  • 主线程执行:确保UI更新在主线程进行

  • 重复执行:设置repeats: true实现连续动画

  • 冲突避免:启动前先停止旧定时器

2. 字符追加逻辑

private func appendNextCharacters(from text: String) {    let currentLength = displayedText.count// 边界检查:防止越界访问    guard currentLength < text.count else {        stopAnimation() // 动画完成,清理资源        return    }// 计算下一次显示的字符范围    let endIndex = min(currentLength + chunkSize, text.count)    let startIndex = text.index(text.startIndex, offsetBy: currentLength)    let targetIndex = text.index(text.startIndex, offsetBy: endIndex)// 提取新字符并追加到显示文本    let newChars = text[startIndex..<targetIndex]    displayedText.append(String(newChars))// 检查动画是否完成    if displayedText.count >= text.count {        stopAnimation()    }}

字符处理细节:

  • Unicode安全:使用String.Index正确处理多字节字符

  • 边界保护:使用min()函数防止数组越界

  • 增量更新:每次只追加新字符,避免重复渲染

  • 3.5 视图渲染策略

1. 条件化渲染

var body: some View {    Group {        if let text = text, !text.isEmpty {            if isAssistantMessage && isStreamingMessage && shouldUseTypewriter {                typewriterView(text)  // 动画视图            } else {                staticView(text)      // 静态视图            }        }    }    // 生命周期绑定    .onAppear { /* 启动动画 */ }    .onDisappear { /* 清理资源 */ }    .onChange(of: text) { /* 处理文本变化 */ }    .onChange(of: isStreamingMessage) { /* 处理流式状态变化 */ }}

2. Markdown支持

@ViewBuilderprivate func typewriterView(_ text: String) -> some View {    if messageUseMarkdown {        Markdown(displayedText)            .markdownBlockStyle(\.blockquote) { configuration in                configuration.label                    .padding()                    .markdownTextStyle {                        FontSize(13)                        FontWeight(.light)                        BackgroundColor(nil)                    }                    .overlay(alignment: .leading) {                        Rectangle()                            .fill(Color.gray)                            .frame(width: 4)                    }                    .background(Color.gray.opacity(0.2))            }    } else {        Text(displayedText)    }}

  • 3.6 生命周期管理

1. 资源自动管理

.onAppear {    if let text = text, isAssistantMessage && isStreamingMessage && shouldUseTypewriter {        startTypewriterAnimation(for: text)    } else if let text = text {        displayedText = text    }}.onDisappear {    stopAnimation() // 防止内存泄漏}

2. 状态变化响应

.onChange(of: isStreamingMessage) { oldIsStreaming, newIsStreaming in    if !newIsStreaming {        // 流式传输结束,立即显示完整内容        if let text = text {            displayedText = text        }        stopAnimation()    }}

3. 内存泄漏防护

private func stopAnimation() {    animationTimer?.invalidate()  // 停止定时器    animationTimer = nil          // 释放引用}

总结

综上,结合三层的优化,通过以上多层协同优化方案,我们成功地将一个卡顿、生硬的文字输出体验转变为流畅、自然的现代化AI交互界面。

PS:如果觉得文章对你有帮助或者启发,欢迎 Star MNN :https://github.com/alibaba/MNN

团队介绍

本文作者揽清,来自淘天集团-Meta技术团队。本团队目前负责面向消费场景的3D/XR基础技术建设和创新应用探索,创造以手机及XR 新设备为载体的消费购物新体验。团队在端智能、端云协同、商品三维重建、真人三维重建、3D引擎、XR引擎等方面有着深厚的技术积累,先后发布深度学习引擎MNN、商品三维重建工具Object Drawer、3D真人数字人TaoAvatar、端云协同系统Walle等。团队在OSDI、MLSys、CVPR、ICCV、NeurIPS、TPAMI等顶级学术会议和期刊上发表多篇论文。欢迎视觉算法、3D/XR引擎、深度学习引擎研发、终端研发等领域的优秀人才加入,共同走进3D数字新时代。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

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

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

相关文章

【开发技巧】VS2022+QT5+OpenCV4.10开发环境搭建QT Creator

VS2022编译器支持配置 QT5默认安装以后支持的是VS2015与VS2017&#xff0c;不支持VS2022&#xff0c;所以必须首先在Qt Creator中配置支持VS2022。配置顺序如下&#xff1a; 首先打开【工具】->【选项】 然点击Kits里面的【编译器】选项。点击Manual下面的【C】然后点击【…

【Linux系统】动静态库的制作

前言&#xff1a; 上文我们讲到了文件系统【Linux系统】详解Ext2&#xff0c;文件系统-CSDN博客 本文我们来讲讲动静态库的制作 库 【Linux】编译器gcc/g及其库的详细介绍_linux gcc 有哪些库-CSDN博客 这篇文章的第4大点&#xff0c;简单是介绍了一下库的基本概念。 静态库 静…

链式二叉树的基本操作——遍历

本文笔者将带领读者一起学习链式二叉树的一些基本语法&#xff0c;至于更难一些的插入删除等&#xff0c;笔者将在后续C更新后再次详细带领大家学习。 首先&#xff0c;在进行二叉树之前&#xff0c;我们需要一颗二叉树&#xff0c;而二叉树的初始化现阶段实现不太现实&#x…

Windows运维之以一种访问权限不允许的方式做了一个访问套接字的尝试

一、问题场景 在Windows 上运维服务过程中&#xff0c;经常会遇到运行服务&#xff0c;部署安装时候无任何问题&#xff0c;后续再某个特殊时间点&#xff0c;突然服务无法启动了。再次启动时&#xff0c;提示端口占用与以一种访问权限不允许的方式做了一个访问套接字的尝试。 …

2020/12 JLPT听力原文 问题二 3番

3番&#xff1a;レストランで、女の人と店長が話しています。店長はサラダについて、どんなアドバイスをしていますか。女&#xff1a;店長、この前話してた新しいランチメニューのサラダを作ってみたんですが、どうでしょうか。 男&#xff1a;ああ、サラダだけで満足できるっ…

芯片行业主要厂商

作为一个小白&#xff0c;每次淘宝买芯片时看到相似的命名规则&#xff1a;“OPA、AD、LT、MAX”等等时&#xff0c;我不禁好奇这些芯片行业大厂有哪些&#xff0c;所以查了些资料&#xff1a; 1. 德州仪器&#xff08;Texas Instruments, TI&#xff09; 公司概况&#xff1…

【BLE系列-第四篇】从零剖析L2CAP:信道、Credit流控、指令详解

目录 引言 一、L2CAP主要功能 二、L2CAP帧格式及信道概念 2.1 逻辑链路是什么&#xff1f; 2.2 逻辑信道的作用 2.3 L2CAP帧格式介绍 三、L2CAP信令信道 3.1 信令信道帧格式说明 3.2 信令信道指令介绍 3.2.1 信令信道指令一览表 3.2.2 Credit流控规则 引言 在BLE协…

CSS保持元素宽高比,固定元素宽高比

方法一&#xff1a; <div class"hcp-fixed-aspect-ratio-box">这里是正文内容 </div>.hcp-fixed-aspect-ratio-box {width: 50%;color: #FFFFFF;margin: 100px auto;background: #FF0000;/* 宽高比2:1&#xff0c;兼容性可能不太好 */aspect-ratio: 2 / …

数据分析小白训练营:基于python编程语言的Numpy库介绍(第三方库)(上篇)

&#xff08;一&#xff09;Numpy库的安装安装指定版本的Numpy库&#xff0c;打开命令提示符&#xff0c;输入下图内容&#xff0c;只需要将1.25.5的版本修改成个人需要的版本&#xff0c;然后按下回车键&#xff0c;numpy库就安装在python中&#xff1a;指定版本numpy库安装可…

从 Windows 到 Linux 服务器的全自动部署教程(免密登录 + 压缩 + 上传 + 启动)

一、准备工作 1. 环境说明 本地开发环境&#xff1a;Windows 服务器&#xff08;需执行部署脚本&#xff09;目标服务器&#xff1a;Linux 服务器&#xff08;需安装 node.js、pm2、unzip&#xff09;核心工具&#xff1a;7-Zip&#xff08;压缩&#xff09;、OpenSSH&#x…

智能汽车领域研发,复用云原始开发范式?

汽车电子电气架构演进趋势&#xff1a;分散的功能ECU -> 域控制器 -> 中央计算服务器汽车电子方案与架构在发展与迭代时会使用虚拟化方法几种可行的软硬一体化方案&#xff1a;多ECU&#xff0c;硬件隔离&#xff0c;硬件分区&#xff0c;车规级多核硬件架构 Hypervisor…

数据电台询价的询价要求

技术规格及主要参数 1.电台基本要求&#xff1a; 1.1 电台中的信号处理基于FPGA设计&#xff0c;采用FPGAARM高速AD/DA设计架构&#xff1b; 1.2 具备频谱感知、自主选频、跳频、扩频等功能&#xff1b; 1.3 具备链路质量信息、自组网路由信息、电池电压监测信息、北斗定位信息…

IoT/HCIP实验-5/基于WIFI的智慧农业实验(LwM2M/CoAP+PSK+ESP8266 连接到 IoTDA)

文章目录概述WIFI8266 通信模组WIFI模组也用AT指令&#xff1f;ESP8266 内置协议栈?支持的无线网络模式MCU通过串口与模组交互Wifi模组做客户端PC-AT接入路由器向本地TCP服务发数据用代码接入你家路由器已接入AP&#xff08;你家Wifi&#xff09;平台侧开发工程配置和编译工程…

定时器输出PWM波配置(呼吸灯)

使用定时器 4 通道 3 生成 PWM 波控制 LED1 &#xff0c;实现呼吸灯效果。 频率&#xff1a;2kHz&#xff0c;PSC71&#xff0c;ARR499pwm.c:#include "pwm.h" // 本模块头文件&#xff1a;应声明 pwm_init/pwm_compare_set 等原型、并包含 HAL 头//&#xff08;示…

[ai-agent]环境简介之沙盒e2b vs daytona

所谓的环境的就是agent运行在哪里&#xff0c;或者是agent和那里进行交互。 最常见的环境就是本地开发环境&#xff0c;也就是个人主机&#xff0c;但是存在问题就是没有办法出网和横向扩展。 在沙盒之前也是有其他选择的&#xff1a; 云服务器&#xff0c; 虚拟机&#xff0c;…

【前端面试题】前端面试知识点(第三十一题到第六十一题)

三十一. CSS实现垂直水平居中 实现元素的垂直水平居中是前端开发中的常见需求,主要有以下几种思路: text-align + line-height实现单行文本水平垂直居中 适用于单行文本元素,通过text-align: center实现水平居中,line-height等于容器高度实现垂直居中 text-align + vertic…

嵌入式练习项目——————抓包获取天气信息

一、内容 尝试通过实时天气接口 - 数据接口 - NowAPI此网站获取天气信息&#xff0c;实现可以发送城市查询当前天气和未来天气 二、获取请求报文 可以根据测试示例看到获取内容&#xff0c;此时数据是cJSON格式&#xff0c;我们首先要通过合适的网址抓包获取到请求报文&#x…

Python爬虫实战:研究NewsCrawl ,构建新浪和网易新闻数据采集系统

1. 引言 1.1 研究背景与意义 在信息时代,新闻作为社会动态、公众观点的重要载体,其传播速度与影响力持续扩大。传统的人工筛选与采集方式已无法满足对海量新闻数据的高效处理需求,亟需自动化工具实现大规模、结构化的新闻数据采集。网络爬虫技术作为一种按照预设规则自动抓…

PyTorch神经网络工具箱全解析:nn.Module vs nn.functional

&#x1f50d; 为何需要神经网络工具箱&#xff1f; 在仅用 Autograd 和 Tensor 实现模型时&#xff0c;开发者需手动设置参数梯度&#xff08;requires_gradTrue&#xff09;、反向传播&#xff08;backward()&#xff09;及梯度提取&#xff0c;过程繁琐且易出错。nn 工具箱应…

Java注解学习记录

目录 一、为什么要学注解&#xff1f; 二、注解是什么&#xff1f; 三、为什么要使用注解&#xff1f; 四、注解的作用 五、注解的分类 5.1 元注解 Retention&#xff08;/ rɪˈtenʃ(ə)n /&#xff09; ★★★★★ Target ★★★★★ Inherited(/ ɪnˈherɪtɪd /…