TypeWell全攻略(四):AI键位分析,让数据开口说话

## 写在前面:为什么我们需要 AI?

我们得先想明白一个问题。

前三篇我们干了什么?我们让 TypeWell 能听见 每一次敲击,能看见 手指的压力分布,还能在累的时候放松一下。

但你想过没有------用户看到满键盘的红红黄黄,看到"左手食指 1024 次",然后呢?

然后他们该干嘛?

这就是我要跟你聊的认知缺口

数据是数据,知识是知识,行动是行动。这三者之间,隔着一条鸿沟。

普通工具给你数据,让你自己想办法。好一点的工具给你统计,让你自己分析。但 TypeWell 想做的是给你行动指南------直接告诉你"该怎么做"。

这就是 AI 键位分析的价值:把冷冰冰的数字,变成有温度的人话


开源仓库地址:Gitee TypeWell 雪豹同志
如果对你有帮助,欢迎 Star ⭐️


一、先搭骨架:整个功能的流程设计

写代码之前,我们要先想清楚一件事:用户是怎么用这个功能的?

把你自己想象成一个普通用户:

  1. 你用了半天 TypeWell,键盘上已经五颜六色了
  2. 你好奇:"我这样打字到底健不健康?"
  3. 你点了"AI键位分析"按钮
  4. 页面跳转,看到一行字:"正在获取你的键盘使用数据..."
  5. 然后文字开始一行一行往外蹦,像有人在打字一样
  6. 最后看到:"建议你把 E 键的活儿分给右手中指一点"

这个流程里,藏着几个必须解决的技术问题

用户感受 背后技术
一点按钮就跳转 前端跳转逻辑
页面自动开始分析 后端检测页面加载
"正在获取数据" 进度提示
文字一行一行蹦 流式响应
像有人在打字 实时渲染
最后给建议 提示词工程

这一篇,我们就一个一个解决。


二、UI 触发:从点击到开始分析

先看最简单的部分:用户点了按钮,怎么跳转到分析页面?

打开 index.html,找到那个"AI键位分析"按钮:

html 复制代码
<button class="btn focus-btn" id="aiAnalysisBtn">AI键位分析</button>

然后在 script.js 里找到它的点击事件:

javascript 复制代码
document.getElementById('aiAnalysisBtn').addEventListener('click', function() {
    window.location.href = 'ai-analysis.html';
});

就这么简单------window.location.href 一赋值,页面就跳了。

思考一下:为什么不用 AJAX 局部刷新,而要整个页面跳转?

因为 AI 分析页面是独立的功能模块 ,有自己的布局、自己的样式、自己的交互逻辑。硬塞在主页里,会让代码乱成一锅粥。分而治之,是程序员的基本素养。


三、自动触发:页面加载完就开始分析

现在用户到了 ai-analysis.html,看到了一个漂亮的加载动画。然后呢?

等着用户再点一次"开始分析"吗?没必要。

好的设计是替用户多想一步 ------用户既然点进来了,就是想分析。那我们就在页面加载完自动开始

怎么实现?看 main.py 里的 on_page_loaded 方法:

python 复制代码
def on_page_loaded(self, ok):
    if ok:
        # 页面加载成功的逻辑
        current_url = self.webview.url().toString()
        
        # 关键判断:当前是不是 AI 分析页面?
        if 'ai-analysis.html' in current_url:
            print("检测到 AI 分析页面,开始分析键盘数据")
            self.analyze_keyboard_data()  # 自动开始分析
        else:
            self.update_frontend()  # 普通页面就正常更新

这里有个重要的思维方法根据上下文决定行为

同一个 on_page_loaded 函数,不能写死一种行为。要根据当前是哪个页面,决定干什么:

  • 在主页 → 更新热力图
  • 在分析页 → 开始 AI 分析
  • 在专注模式 → 啥也不干(动画自己跑)

这就是状态驱动的编程思想。


四、数据准备:喂给 AI 之前要加工

现在进入了 analyze_keyboard_data 函数。第一步是告诉用户"我正在干活":

python 复制代码
def analyze_keyboard_data(self):
    # 第一步:给用户反馈
    self.update_frontend_signal.emit('progress', '正在获取键盘使用数据...')

为什么要先发这个信号?

因为用户心理有个耐受时间------超过 0.5 秒没反应,就会觉得"卡了"。超过 2 秒没反应,就可能关掉重开。

所以我们要先给个反馈:"我收到了,正在干活,别急。"


4.1 从数据库读数据

python 复制代码
try:
    conn = sqlite3.connect('keyboard_heatmap.db')
    c = conn.cursor()
    c.execute("SELECT key, count FROM key_usage")
    keyboard_data = dict(c.fetchall())
    conn.close()

这里有个细节:dict(c.fetchall()) 直接把查询结果转成了字典。因为 fetchall() 返回的是 [(key1, count1), (key2, count2)] 这样的列表,传给 dict() 正好变成 {key1: count1, key2: count2}

思考:为什么不直接用列表,非要转成字典?

因为后面我们要频繁地按键名取值 ------left_hits = sum(keyboard_data.get(key, 0) for key in left_hand_keys)。字典的查询是 O(1),列表查询是 O(n)。一万次按键数据,用列表能卡死。

这就是数据结构决定算法效率的道理。


4.2 处理空数据的情况

python 复制代码
if total_hits == 0:
    self.update_frontend_signal.emit('error', '暂无键盘使用数据,请先使用键盘后再进行分析')
    return

这里又是一个设计点:用户可能刚打开软件就点进来,数据库里一条数据都没有。这时候强行分析,AI 也只能说"没有数据",没意义。

所以我们要提前拦截,给个友好的提示,然后 return 出去。

防御性编程的思维:不要假设用户会按你的预期操作。


4.3 计算统计指标

接下来是最关键的数据加工环节:

python 复制代码
# 总次数
total_hits = sum(keyboard_data.values())

# 最热按键
hottest_key = max(keyboard_data, key=keyboard_data.get)
hottest_key_count = keyboard_data[hottest_key]

max(keyboard_data, key=keyboard_data.get) 这行代码值得讲一讲:

  • keyboard_data 是字典,直接 max() 会取最大的(按字母序)
  • 但我们想要的是值最大的那个键
  • 所以传入 key=keyboard_data.get,告诉 max:比较的时候用 keyboard_data.get(每个键) 的结果来比

这是 Python 里一个很实用的技巧,记下来。


4.4 左右手分区

python 复制代码
left_hand_keys = ['q', 'w', 'e', 'r', 't', 'a', 's', 'd', 'f', 'g', 
                  'z', 'x', 'c', 'v', 'b', '1', '2', '3', '4', '5',
                  'tab', 'caps', 'left shift', 'left ctrl']

right_hand_keys = ['y', 'u', 'i', 'o', 'p', 'h', 'j', 'k', 'l', ';',
                   'n', 'm', ',', '.', '/', '6', '7', '8', '9', '0',
                   '-', '=', 'backspace', 'enter', 'right shift', 'right alt']

注意看 :这里用的是 left shiftright shift,不是统一的 shift

这就是第一篇里我们保留左右键的原因------为了这一刻的分析。

如果当时我们图省事,把所有 shift 都统一成 shift,现在就分不清左手小指和右手小指谁更累了。

设计的延续性:好的设计要为未来留余地。

python 复制代码
left_hits = sum(keyboard_data.get(key, 0) for key in left_hand_keys)
right_hits = sum(keyboard_data.get(key, 0) for key in right_hand_keys)

hand_balance = round((left_hits / total_hits) * 100) if total_hits > 0 else 50

sum() 里用生成器表达式,比写循环更简洁。keyboard_data.get(key, 0) 保证了即使某个键没出现过,也不会报错,而是返回 0。


五、提示词工程:教 AI 说人话

这是全篇最难的部分,也是 AI 分析能不能用的关键

很多初学者会这样写提示词:

python 复制代码
prompt = f"分析这些数据:{keyboard_data}"

结果 AI 返回:

复制代码
根据数据分析,您按了 q 键 23 次,w 键 45 次,e 键 67 次...

废话,用户自己不会看吗?

我们要的是洞察,不是复述。


5.1 结构化思维

先想清楚:一份好的键位分析,应该包含哪些内容?

我自己列了个清单:

  1. 总体情况:今天敲了多少次,大概什么强度
  2. 左右手平衡:是不是一只手在摸鱼,另一只手在拼命
  3. 手指负荷:哪根手指最累,哪根最闲
  4. 异常模式:有没有不合理的按键习惯
  5. 具体建议:可以怎么改进
  6. 装备建议:需不需要换键盘

有了这个清单,才知道提示词里该要什么。


5.2 完整的提示词模板

python 复制代码
def generate_analysis_prompt(self, keyboard_data):
    # 前面计算的统计指标
    # ...
    
    prompt = f"""请分析以下键盘使用数据,并提供个性化的键位使用建议:

## 一、统计数据
- 总敲击次数:{total_hits}
- 最热按键:{hottest_key} ({hottest_key_count}次)
- 左手敲击:{left_hits} 次
- 右手敲击:{right_hits} 次
- 左右平衡:{hand_balance}% 左手,{100 - hand_balance}% 右手

## 二、详细键位数据(使用频率最高的 20 个按键)
"""
    # 按次数排序,只显示前 20
    sorted_keys = sorted(keyboard_data.items(), key=lambda x: x[1], reverse=True)[:20]
    for key, count in sorted_keys:
        prompt += f"- {key}: {count}次\n"
    
    prompt += """
## 三、请从以下维度进行分析
1. **使用模式分析**:用户主要用哪些手指?打字习惯偏向左手还是右手?
2. **潜在风险识别**:是否存在左右手严重不平衡?某根手指负荷是否过重?
3. **具体改进建议**:给出可操作的调整方案,例如"尝试将部分 E 键操作分配给右手中指"
4. **键盘布局建议**:如果适用,推荐更适合用户的键盘布局或设备

## 四、输出要求
- 分析要专业、详细
- 不要使用表格(用户看不懂)
- 语气要像一位温和的健康教练,而不是冷冰冰的机器
- 给出具体、可执行的动作建议
"""
    return prompt

5.3 提示词设计的四个原则

原则一:结构化

## 一、统计数据 这样的标题把内容分成块。AI 看到这种结构,就知道应该按这个框架输出。

为什么? 因为 AI 的训练数据里,这种格式的文章通常质量较高。你给 AI 什么格式,它倾向于用类似的格式回复。

原则二:给例子

"例如'尝试将部分 E 键操作分配给右手中指'" 这句话很关键。

AI 需要知道你说的"具体建议"长什么样。不给例子,它可能给出"请改善您的打字习惯"这种正确的废话。给了例子,它就知道要模仿这种格式。

原则三:限制范围

"使用频率最高的 20 个按键"------为什么是 20,不是全部?

因为用户根本看不完。人脑的短期记忆容量是有限的,给太多数据,用户反而抓不住重点。少即是多

原则四:定调子

"语气要像一位温和的健康教练"------这很重要。

同样的内容,用不同的语气说,效果天差地别:

"左手食指负荷过重,建议调整"

(像医生诊断,冷冰冰)
"你左手食指今天辛苦了,按了 1024 次。要不要试试让它休息一下?"

(像朋友关心,有温度)

AI 可以模仿这两种语气,关键看你怎么引导。


六、流式响应:让用户看着 AI 思考

提示词准备好了,接下来是怎么调用 API。

6.1 为什么不能在主线程调用?

这是很多初学者会犯的错误:

python 复制代码
# ❌ 错误示范
def analyze_keyboard_data(self):
    response = client.chat.completions.create(...)  # 可能耗时 3-5 秒
    # 这 3-5 秒里,整个窗口卡死,无法响应任何操作

网络请求是不可控的------可能 1 秒返回,也可能 10 秒。在这期间,UI 线程被阻塞,用户点哪里都没反应,体验极差。

黄金法则:任何可能耗时的操作,都要放到子线程。

6.2 子线程的写法

python 复制代码
def analyze_keyboard_data(self):
    # ... 前面的数据准备
    
    # 启动子线程处理 AI 分析
    import threading
    thread = threading.Thread(target=self.process_ai_analysis, args=(prompt,))
    thread.daemon = True  # 设为守护线程,主线程退出时自动结束
    thread.start()
    
    # 主线程立刻返回,继续处理 UI 事件

daemon = True 的意思是:这个线程是"守护"线程。如果主程序退出了,这个线程会自动结束,不会拖后腿。


6.3 流式响应的实现

python 复制代码
def process_ai_analysis(self, prompt):
    """在单独的线程中处理 AI 分析"""
    try:
        from openai import OpenAI
        
        # 初始化客户端(注意:这里是模力方舟的地址)
        client = OpenAI(
            base_url="https://api.moark.com/v1",
            api_key="你的 API 密钥",  # ⚠️ 提醒读者替换成自己的
        )
        
        # 发送请求,启用 stream=True
        response = client.chat.completions.create(
            messages=[
                {
                    "role": "system",
                    "content": "You are a helpful and harmless assistant. You should think step-by-step."
                },
                {
                    "role": "user",
                    "content": prompt
                }
            ],
            model="DeepSeek-V3.2-Exp",
            stream=True,  # ⚠️ 核心参数!
            max_tokens=10240,
            temperature=0.7,
        )
        
        # 处理流式响应
        for chunk in response:
            # 检查窗口是否正在关闭
            if self.is_closing:
                break
                
            if len(chunk.choices) == 0:
                continue
                
            delta = chunk.choices[0].delta
            
            # 区分推理内容和最终内容
            if hasattr(delta, 'reasoning_content') and delta.reasoning_content:
                # 推理内容:模型的思考过程
                self.update_frontend_signal.emit('reasoning', delta.reasoning_content)
                
            elif delta.content:
                # 最终内容
                self.update_frontend_signal.emit('content', delta.content)
                
    except Exception as e:
        error_msg = f"AI 分析失败:{str(e)}"
        self.update_frontend_signal.emit('error', error_msg)

这里有几个细节要讲:

细节一:为什么用 hasattr(delta, 'reasoning_content')

因为不是所有模型都支持推理过程展示。DeepSeek 系列的模型会返回 reasoning_content 字段,里面是模型的"思考过程"。普通模型只有 content

hasattr 判断一下,有就展示,没有就跳过------兼容性思维

细节二:if self.is_closing: break 的作用

如果用户等得不耐烦,直接关了窗口,这个循环还在跑,浪费资源。所以每拿到一个 chunk,都检查一下窗口还在不在。不在了就赶紧停。

细节三:len(chunk.choices) == 0 的处理

流式响应里,有些 chunk 可能不包含内容(比如心跳包)。直接跳过就行,不影响。


七、线程通信:从子线程到 UI 线程

现在有个问题:子线程里拿到了 AI 返回的内容,怎么更新 UI?

直接写 self.webview.page().runJavaScript(...) 会报错,因为 Qt 不允许在子线程操作 UI。

解决方案是 pyqtSignal

7.1 定义信号

python 复制代码
class KeyboardHeatmapApp(QMainWindow):
    # 定义信号:参数是 (类型, 内容)
    update_frontend_signal = pyqtSignal(str, str)
    
    def __init__(self):
        super().__init__()
        # 连接信号到槽函数
        self.update_frontend_signal.connect(self._update_frontend_slot)

思考 :为什么是 pyqtSignal(str, str),不是 pyqtSignal(dict)

因为信号传递的是跨线程消息,要尽量轻量。传两个字符串,比传一个字典更可靠、更高效。


7.2 发送信号(在子线程里)

python 复制代码
# 在 process_ai_analysis 里
self.update_frontend_signal.emit('reasoning', delta.reasoning_content)
self.update_frontend_signal.emit('content', delta.content)
self.update_frontend_signal.emit('error', error_msg)
self.update_frontend_signal.emit('progress', '正在获取数据...')

第一个参数是类型 ,第二个参数是内容。类型告诉槽函数"这是什么",内容告诉它"具体是什么"。


7.3 接收信号(在主线程里)

python 复制代码
def _update_frontend_slot(self, update_type, content):
    """这个函数在主线程执行,可以安全地操作 UI"""
    if self.is_closing:
        return
        
    # 检查当前是不是 AI 分析页面
    current_url = self.webview.url().toString()
    if 'ai-analysis.html' not in current_url:
        return
        
    try:
        if update_type == 'content':
            escaped = json.dumps(content)
            self.webview.page().runJavaScript(
                f'window.appendAnalysisResult({{"type": "content", "content": {escaped}}})'
            )
        elif update_type == 'reasoning':
            escaped = json.dumps(content)
            self.webview.page().runJavaScript(
                f'window.appendAnalysisResult({{"type": "reasoning", "content": {escaped}}})'
            )
        elif update_type == 'error':
            escaped = json.dumps(content)
            self.webview.page().runJavaScript(
                f'window.updateAnalysisResult({{"error": {escaped}}})'
            )
        elif update_type == 'progress':
            escaped = json.dumps(content)
            self.webview.page().runJavaScript(
                f'window.updateAnalysisResult({{"progress": {escaped}}})'
            )
    except Exception as e:
        if not self.is_closing:
            print(f"更新前端失败: {e}")

几个细节:

细节一:json.dumps(content) 的作用

content 是字符串,里面可能包含引号、换行符。直接拼接到 JavaScript 代码里会语法错误。json.dumps 会把这些特殊字符转义成安全的形式。

细节二:为什么有两个前端函数?

  • appendAnalysisResult:追加内容(用于流式输出)
  • updateAnalysisResult:覆盖内容(用于错误/进度)

职责不同,分开写更清晰。

细节三:try...except 的必要性

runJavaScript 可能失败(比如页面正在关闭)。捕获异常,避免程序崩溃。


八、前端渲染:让文字动起来

现在看前端怎么接收这些内容。

8.1 缓存机制

javascript 复制代码
// 累积 Markdown 内容
let markdownBuffer = '';

为什么要缓存?因为流式输出是一块一块来的,每来一块,我们要把之前的内容加上新内容,重新渲染。


8.2 追加内容

javascript 复制代码
window.appendAnalysisResult = function(data) {
    const analysisResult = document.getElementById('analysisResult');
    
    if (data.type === 'reasoning') {
        // 推理内容:追加到缓存
        markdownBuffer += data.content;
        analysisResult.innerHTML = parseMarkdown(markdownBuffer);
        
        // 给推理内容加灰色样式(通过 CSS)
        // 可以在 parseMarkdown 里给特定内容加类,这里简化了
        
    } else if (data.type === 'content') {
        // 最终内容:追加到缓存
        markdownBuffer += data.content;
        analysisResult.innerHTML = parseMarkdown(markdownBuffer);
    }
    
    // 自动滚动到底部,让用户看到最新内容
    analysisResult.scrollTop = analysisResult.scrollHeight;
};

为什么每次都要重新 innerHTML

因为浏览器不知道你追加了什么,只能重新渲染整个区域。这是最简单的实现,性能也够用(每秒几十次追加,完全没问题)。


8.3 覆盖内容

javascript 复制代码
window.updateAnalysisResult = function(data) {
    const analysisResult = document.getElementById('analysisResult');
    
    if (data.error) {
        analysisResult.innerHTML = `
            <div class="error-container">
                <div class="error-icon">❌</div>
                <p class="error-title">分析失败</p>
                <p class="error-message">${data.error}</p>
            </div>
        `;
        markdownBuffer = '';  // 清空缓存
        
    } else if (data.progress) {
        analysisResult.innerHTML = `
            <div class="loading-container">
                <div class="loading"></div>
                <p class="loading-text">${data.progress}</p>
            </div>
        `;
        // 注意:这里不改变 markdownBuffer
        // 因为进度提示只是临时覆盖,AI 返回后要恢复之前的缓存
    }
};

为什么错误时要清空缓存?

因为出错了,之前的缓存没意义了。留着反而可能让用户困惑。

为什么进度时不改变缓存?

因为进度提示是临时的,等 AI 开始返回内容,我们要用之前的缓存继续追加。


8.4 Markdown 解析

javascript 复制代码
function parseMarkdown(text) {
    let html = text;
    
    // 第一步:保护代码块(防止里面的内容被后面的规则误处理)
    const codeBlocks = [];
    html = html.replace(/```([\s\S]*?)```/g, function(match, code) {
        codeBlocks.push(escapeHtml(code));
        return '|||CODE_BLOCK_' + (codeBlocks.length - 1) + '|||';
    });
    
    // 第二步:处理标题
    html = html.replace(/^###### (.*$)/gm, '<h6>$1</h6>');
    html = html.replace(/^##### (.*$)/gm, '<h5>$1</h5>');
    html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>');
    html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
    html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
    html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
    
    // 第三步:处理行内样式
    html = html.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>');
    html = html.replace(/\*([^*]+?)\*/g, '<em>$1</em>');
    html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
    
    // 第四步:处理链接
    html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
    
    // 第五步:处理换行
    html = html.replace(/\n/g, '<br>');
    
    // 第六步:恢复代码块
    html = html.replace(/\|\|\|CODE_BLOCK_(\d+)\|\|\|/g, function(match, index) {
        return '<pre><code>' + codeBlocks[parseInt(index)] + '</code></pre>';
    });
    
    return html;
}

function escapeHtml(text) {
    return text
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}

这个解析器的设计思路:

第一步先保护代码块 ,因为代码块里的内容可能包含 #* 等 Markdown 语法,如果不保护起来,后面的替换会破坏它们。

最后一步恢复代码块,这时其他语法已经处理完了,可以安全地把代码块放回去。

这就是分层处理的思维:把复杂问题拆成几个简单的步骤,每一步只关注一件事。


九、错误处理:AI 也会犯错

9.1 API 调用失败

python 复制代码
except Exception as api_error:
    error_message = f"AI 分析失败:{str(api_error)}"
    self.update_frontend_signal.emit('error', error_message)

为什么把错误信息也发到前端?

因为用户需要知道发生了什么。如果是网络问题,用户可以去检查网络;如果是 API Key 问题,用户可以去配置中心修改。透明的错误信息,比"出错了"更有用。


9.2 没有数据

python 复制代码
if total_hits == 0:
    self.update_frontend_signal.emit('error', '暂无键盘使用数据,请先使用键盘后再进行分析')
    return

这个提示很关键------不是"分析失败",而是"暂无数据,先去用用键盘"。

用户看到这个提示,就知道不是程序坏了,而是自己还没产生数据。引导性的错误提示,比单纯的报错更好。


9.3 用户中途退出

python 复制代码
if self.is_closing:
    break  # 停止处理

这个检查放在循环里,每拿到一个 chunk 都检查一次。用户关窗口后,最多再处理一个 chunk 就停下,不会浪费资源。


十、完整流程回顾

现在我们从头到尾串一遍:

用户视角:

  1. 点"AI键位分析"按钮,页面跳转
  2. 看到"正在获取数据..."
  3. 看到文字一行一行往外蹦
  4. 最后看到完整的分析建议
  5. 不满意可以点"再次分析",重新来一遍

技术视角:

复制代码
点击按钮 → window.location.href 跳转
    ↓
页面加载完成 → on_page_loaded 检测到 AI 页面
    ↓
analyze_keyboard_data() 被调用
    ↓
发送 'progress' 信号 → 前端显示"正在获取数据..."
    ↓
从数据库读取数据
    ↓
如果没有数据 → 发送 'error' 信号 → 前端显示错误
    ↓
如果有数据 → 生成提示词
    ↓
启动子线程 process_ai_analysis()
    ↓
子线程调用模力方舟 API(stream=True)
    ↓
逐块接收响应
    ├─ 有 reasoning_content → 发送 'reasoning' 信号
    └─ 有 content → 发送 'content' 信号
    ↓
主线程 _update_frontend_slot 接收信号
    ↓
runJavaScript 调用前端函数
    ↓
前端追加内容到 analysisResult
    ↓
自动滚动到底部
    ↓
(如果出错)发送 'error' 信号 → 前端显示错误

十一、踩坑总结(过来人的血泪史)

问题 现象 原因 解决方案
UI 卡死 点分析后窗口无响应 API 调用在主线程 放子线程
无法更新 UI 子线程里调 runJavaScript 报错 Qt 的线程限制 用 pyqtSignal
流式不工作 等了半天才一次性出现 忘了加 stream=True 检查参数
推理内容没显示 只有最终结果 模型不支持 用 hasattr 判断
内容重复 同一个内容显示两次 前端重复追加 检查逻辑
XSS 风险 AI 返回的内容带 script 标签 没转义 escapeHtml
API Key 泄漏 代码提交到 GitHub 被发现 硬编码在代码里 放环境变量
用户等不及关窗口 程序还在后台跑 没检查 is_closing 循环里加判断
错误提示太模糊 用户不知道发生了什么 报错信息太简略 把具体错误传前端

写在最后

这一篇我们讲了这么多,其实核心就是三件事:

  1. 提示词工程:怎么让 AI 输出有用的健康建议
  2. 流式响应:怎么让用户看着 AI 思考
  3. 线程通信:怎么在子线程和 UI 线程之间传数据

但最重要的不是这些代码本身,而是背后的思维方法

  • 分而治之:复杂问题拆成简单步骤
  • 状态驱动:根据上下文决定行为
  • 防御性编程:不要假设用户会按预期操作
  • 兼容性思维:考虑不同模型、不同场景
  • 透明错误:给用户有用的信息,而不是敷衍
相关推荐
RunsenLIu1 小时前
智慧房屋租赁管理系统
前端·javascript·vue.js
明月_清风1 小时前
pwa 安装/离线/推送/后台同步 全套高级能力
前端·pwa
cyber_两只龙宝1 小时前
Tomcat--企业级web应用服务器详细介绍与整合Nginx配置流程
linux·运维·前端·nginx·云原生·tomcat·负载均衡
明月_清风1 小时前
Service Worker 和 Workbox 分别是什么?它们有什么区别?
前端·pwa
宇文仲竹1 小时前
为OpenClaw构建双层记忆系统:QMD + Mem0的混合架构实战
ai
heimeiyingwang1 小时前
企业 AI 预算规划:如何分配资源实现最大 ROI
大数据·人工智能
咚咚王者2 小时前
人工智能之视觉领域 计算机视觉 第十四章 人脸检测
人工智能·计算机视觉
程序哥聊面试2 小时前
TypeScript 入门
前端·javascript·typescript
码界筑梦坊2 小时前
220-基于Python的诺贝尔奖数据可视化分析系统
开发语言·python·信息可视化·数据分析·毕业设计·fastapi