<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>AI · 仁才德</title>
    <link>https://jared.lynskey.co.nz/zh-cn/tags/ai/</link>
    <description>关于软件工程与研发团队领导力的短文——绩效评估、招聘与团队扩张、CI/CD、DevOps 监控、智能体 AI。写于首尔。</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <copyright>© 2026 仁才德</copyright>
    <lastBuildDate>Fri, 30 Jan 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/zh-cn/tags/ai/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>React Native 端侧 AI：用 llama.cpp 跑 Qwen 1.7B</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-01-30-on-device-ai/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-01-30-on-device-ai/</guid>
      <pubDate>Fri, 30 Jan 2026 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>React Native</category>
      <category>AI</category>
      <category>LLM</category>
      <category>llama.cpp</category>
      <category>Qwen</category>
      <category>设备端 AI</category>
      <category>隐私</category>
      <category>移动开发</category>
      <description>记录我如何用 llama.rn 和 Qwen 1.7B，把窗帘报价应用里的 AI 助手完全搬到手机上运行。</description>
      <content:encoded><![CDATA[<p>我的窗帘报价应用 Curtain Estimator 里有个 AI 助手，完全跑在手机上。数据一个字节都不出设备。用户用自然语言就能创建工作单、搜索客户、管理项目，开了飞行模式照样能用。</p>
<p>这篇文章讲讲我是怎么用 <strong>llama.rn</strong>（llama.cpp 的 React Native 绑定）和 <strong>Qwen 1.7B</strong> 把它做出来的。这个尺寸的模型，能干的活比我预想的多得多。</p>
<blockquote><p><strong>📝 更新：</strong> 现在的实现已经换成 <a href="https://huggingface.co/unsloth/Qwen3.5-2B-GGUF"  target="_blank" rel="noreferrer">Qwen3.5-2B-GGUF</a>，替代了最初的 Qwen3-1.7B。另外我也不再用 <code>/no_think</code> 消息这种土办法，改用 <code>chat_template_kwargs</code> 参数正式关闭思考模式：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">chat_response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">chat</span><span class="o">.</span><span class="n">completions</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span><span class="o">=</span><span class="s2">&#34;Qwen/Qwen3.5-27B&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">messages</span><span class="o">=</span><span class="n">messages</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_tokens</span><span class="o">=</span><span class="mi">32768</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">temperature</span><span class="o">=</span><span class="mf">0.7</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">top_p</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">presence_penalty</span><span class="o">=</span><span class="mf">1.5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">extra_body</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;top_k&#34;</span><span class="p">:</span> <span class="mi">20</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;chat_template_kwargs&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;enable_thinking&#34;</span><span class="p">:</span> <span class="kc">False</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>比在提示词里塞开关干净多了。</p>
</blockquote>
<h2 class="relative group">我为什么不想用云端 AI
    <div id="我为什么不想用云端-ai" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%88%91%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e6%83%b3%e7%94%a8%e4%ba%91%e7%ab%af-ai" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>移动应用里的 AI 功能，套路基本一样：用户输入消息，应用发给 OpenAI、Anthropic 或 Google，响应回来，账单往上涨。</p>
<p>但这是一个窗帘安装商拿来做生意的应用。照这个套路走，客户的姓名、地址、电话就交给了第三方，项目细节、报价、备注躺在别人的服务器上，还得操心 GDPR 和数据驻留这些我根本不想碰的问题。API 账单也随着用户数一路上涨。</p>
<p>另一条路是把模型直接放到用户手机上跑。我选了这条。</p>

<h2 class="relative group">为什么现在做得成了
    <div id="为什么现在做得成了" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e7%8e%b0%e5%9c%a8%e5%81%9a%e5%be%97%e6%88%90%e4%ba%86" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>放在一年前我不会碰这个，但有三件事几乎同时起了变化。一是量化模型变小了：Q4_K_M 量化后的 Qwen 1.7B 只有 1.1 GB，比大多数游戏还小，下载一次就常驻应用存储。二是手机 GPU 够快了：llama.cpp 在 iOS 上走 Metal，Android 上走 Vulkan，iPhone 14 Pro 能跑到每秒 15 个 token 左右，实时流式输出绰绰有余。三是小模型真的能用了：Qwen 1.7B 能遵循结构化指令、解析 JSON、串起多步推理。业务逻辑要的正是这些能力，又没人指望它写诗。</p>

<h2 class="relative group">选模型
    <div id="选模型" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%80%89%e6%a8%a1%e5%9e%8b" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>定下 Qwen3-1.7B 之前，我试了四个小模型。</p>
<p><strong>TinyLlama 1.1B</strong>（637 MB）最快，但结构化输出始终不稳，动不动就编造客户 ID、漏掉必填字段。<strong>Phi-3-mini</strong>（1.8 GB）推理不错，可惜话太多，20 个字能说清的事非要写 200 字的小作文。<strong>Gemma-2B</strong>（1.2 GB）做分类又快又准，但函数调用不行，我的工具系统需要的 <code>&lt;action&gt;</code> 标签它输出不稳定。<strong>Qwen3-1.7B</strong>（1.1 GB）正好卡在甜点位：结构化输出可靠，指令遵循精确，还支持用 <code>&lt;think&gt;</code> 标签做思维链。</p>
<p>Q4_K_M 量化用的是 4 位权重加 k-means 聚类，体积比全精度小约 75%，质量损失只有 5% 上下。</p>

<h2 class="relative group">配置 llama.rn
    <div id="配置-llamarn" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%85%8d%e7%bd%ae-llamarn" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>llama.rn 把 llama.cpp 封装给 React Native 用，安装是整件事里最简单的部分：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install llama.rn
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ios <span class="o">&amp;&amp;</span> pod install</span></span></code></pre></div></div>
<p>模型本体在首次使用时下载：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">MODEL_URL</span> <span class="o">=</span> <span class="s2">&#34;https://huggingface.co/unsloth/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q4_K_M.gguf&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">MODEL_PATH</span> <span class="o">=</span> <span class="nx">FileSystem</span><span class="p">.</span><span class="nx">documentDirectory</span> <span class="o">+</span> <span class="s2">&#34;llama-models/Qwen3-1.7B-Q4_K_M.gguf&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">downloadModel</span> <span class="o">=</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">downloadResumable</span> <span class="o">=</span> <span class="nx">FileSystem</span><span class="p">.</span><span class="nx">createDownloadResumable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nx">MODEL_URL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">MODEL_PATH</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">{},</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="nx">progress</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">pct</span> <span class="o">=</span> <span class="nx">progress</span><span class="p">.</span><span class="nx">totalBytesWritten</span> <span class="o">/</span> <span class="nx">progress</span><span class="p">.</span><span class="nx">totalBytesExpectedToWrite</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="nx">setDownloadProgress</span><span class="p">(</span><span class="nx">pct</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">downloadResumable</span><span class="p">.</span><span class="nx">downloadAsync</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span></span></span></code></pre></div></div>
<p>WiFi 下两三分钟，LTE 大概五到八分钟。下载完成后，开着 GPU 加速加载：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">initLlama</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">model</span>: <span class="kt">MODEL_PATH</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">n_ctx</span>: <span class="kt">8192</span><span class="p">,</span>      <span class="c1">// 8K context window
</span></span></span><span class="line"><span class="cl">  <span class="nx">n_gpu_layers</span>: <span class="kt">99</span><span class="p">,</span> <span class="c1">// Use GPU for all layers
</span></span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p><code>n_gpu_layers: 99</code> 千万别省。把计算从 CPU 挪到 Metal/Vulkan 上，速度能提 5 倍左右。</p>

<h2 class="relative group">流式推理
    <div id="流式推理" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%b5%81%e5%bc%8f%e6%8e%a8%e7%90%86" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>没人愿意盯着加载动画看十秒钟，所以响应按 token 逐个流出：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">fullResponse</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">llamaContext</span><span class="p">.</span><span class="nx">completion</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">messages</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span> <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;system&#34;</span><span class="p">,</span> <span class="nx">content</span>: <span class="kt">systemPrompt</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span> <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="nx">content</span><span class="o">:</span> <span class="s2">&#34;Create a job for John Smith&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nx">n_predict</span>: <span class="kt">512</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">temperature</span>: <span class="kt">0.7</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">top_p</span>: <span class="kt">0.8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Called for each token
</span></span></span><span class="line"><span class="cl">    <span class="nx">fullResponse</span> <span class="o">+=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">token</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">setStreamingText</span><span class="p">(</span><span class="nx">fullResponse</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span></span></span></code></pre></div></div>
<p>在近几年的手机上，首个 token 约 200ms（这是提示词处理的时间），之后每个 token 50-80ms。跟走网络的 API 一比，体感就是即时的。</p>

<h2 class="relative group">小模型也玩得转的函数调用
    <div id="小模型也玩得转的函数调用" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b0%8f%e6%a8%a1%e5%9e%8b%e4%b9%9f%e7%8e%a9%e5%be%97%e8%bd%ac%e7%9a%84%e5%87%bd%e6%95%b0%e8%b0%83%e7%94%a8" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>GPT-4 那套函数调用靠 JSON schema，小模型在这里会栽跟头：JSON 格式坏掉、必填字段丢失。所以我用了个更简单的 XML 协议：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">systemPrompt</span> <span class="o">=</span> <span class="sb">`You are an AI assistant. When you need to take an action, output:
</span></span></span><span class="line"><span class="cl"><span class="sb">&lt;action&gt;{&#34;type&#34;:&#34;search_customers&#34;,&#34;query&#34;:&#34;Smith&#34;}&lt;/action&gt;
</span></span></span><span class="line"><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="cl"><span class="sb">Available actions:
</span></span></span><span class="line"><span class="cl"><span class="sb">- search_customers: {&#34;type&#34;:&#34;search_customers&#34;,&#34;query&#34;:&#34;John&#34;}
</span></span></span><span class="line"><span class="cl"><span class="sb">- create_job: {&#34;type&#34;:&#34;create_job&#34;,&#34;customer_id&#34;:5,&#34;alias&#34;:&#34;Living Room&#34;}
</span></span></span><span class="line"><span class="cl"><span class="sb">- update_job: {&#34;type&#34;:&#34;update_job&#34;,&#34;job_id&#34;:&#34;J-0042&#34;,&#34;status&#34;:&#34;quoting&#34;}
</span></span></span><span class="line"><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="cl"><span class="sb">Rules:
</span></span></span><span class="line"><span class="cl"><span class="sb">1. ONE action tag per response, at the very end
</span></span></span><span class="line"><span class="cl"><span class="sb">2. When searching/creating → end with &lt;action&gt;
</span></span></span><span class="line"><span class="cl"><span class="sb">3. When just chatting → no action tag
</span></span></span><span class="line"><span class="cl"><span class="sb">`</span><span class="p">;</span></span></span></code></pre></div></div>
<p>选 XML 标签的理由都很朴素：<code>&lt;action&gt;</code> 和 <code>&lt;/action&gt;</code> 的起止毫不含糊，解析只要一条正则，提示词里的示例本身就是格式说明，而且就算模型在标签前后多说几句废话也不影响。</p>
<p>解析代码简单到不好意思展示：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">parseAction</span><span class="p">(</span><span class="nx">text</span>: <span class="kt">string</span><span class="p">)</span><span class="o">:</span> <span class="nx">Action</span> <span class="o">|</span> <span class="kc">null</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">match</span> <span class="o">=</span> <span class="nx">text</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/&lt;action&gt;([\s\S]*?)&lt;\/action&gt;/</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">match</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">match</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nx">trim</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>解析出 action 就执行，再把结果塞回对话里：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">action</span> <span class="o">=</span> <span class="nx">parseAction</span><span class="p">(</span><span class="nx">modelResponse</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">action</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">executeAction</span><span class="p">(</span><span class="nx">action</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Inject result as a system message
</span></span></span><span class="line"><span class="cl">  <span class="nx">conversationHistory</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">content</span><span class="o">:</span> <span class="sb">`[TOOL_RESULT] </span><span class="si">${</span><span class="nx">result</span><span class="p">.</span><span class="nx">message</span><span class="si">}</span><span class="err">\</span><span class="sb">n</span><span class="err">\</span><span class="sb">n→ NEXT: Tell the user what happened.`</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Continue generation
</span></span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">generateNextResponse</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>就这么点东西，多步工作流已经能顺畅跑起来了：</p>
<p><strong>用户</strong>：&ldquo;Create a job for Smith&rdquo;</p>
<ol>
<li><strong>模型</strong>：<code>&lt;action&gt;{&quot;type&quot;:&quot;search_customers&quot;,&quot;query&quot;:&quot;Smith&quot;}&lt;/action&gt;</code></li>
<li><strong>系统</strong>：<code>[TOOL_RESULT] Found: Jane Smith (id:42), Bob Smith (id:89)</code></li>
<li><strong>模型</strong>：&ldquo;找到两位 Smith：Jane 和 Bob。您要哪一位？&rdquo;</li>
<li><strong>用户</strong>：&ldquo;Jane&rdquo;</li>
<li><strong>模型</strong>：<code>&lt;action&gt;{&quot;type&quot;:&quot;create_job&quot;,&quot;customer_id&quot;:42}&lt;/action&gt;</code></li>
<li><strong>系统</strong>：<code>[TOOL_RESULT] Job J-0073 created</code></li>
<li><strong>模型</strong>：&ldquo;完成！已为 Jane Smith 创建工作单 J-0073。&rdquo;</li>
</ol>

<h2 class="relative group">用 &lt;think&gt; 标签做思维链
    <div id="用-think-标签做思维链" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%94%a8-think-%e6%a0%87%e7%ad%be%e5%81%9a%e6%80%9d%e7%bb%b4%e9%93%be" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>让小模型把推理&quot;说出来&quot;，稳定性会肉眼可见地提高。Qwen 自带思考模式：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">systemPrompt</span> <span class="o">=</span> <span class="sb">`Before each response, wrap your reasoning in &lt;think&gt; tags:
</span></span></span><span class="line"><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="cl"><span class="sb">&lt;think&gt;
</span></span></span><span class="line"><span class="cl"><span class="sb">INTENT: what does the user want?
</span></span></span><span class="line"><span class="cl"><span class="sb">HAVE: what data do I already have?
</span></span></span><span class="line"><span class="cl"><span class="sb">NEED: what&#39;s still missing?
</span></span></span><span class="line"><span class="cl"><span class="sb">DECISION: call tool | ask user | just respond
</span></span></span><span class="line"><span class="cl"><span class="sb">&lt;/think&gt;
</span></span></span><span class="line"><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="cl"><span class="sb">Then output your actual response.`</span><span class="p">;</span></span></span></code></pre></div></div>
<p>模型内部大概是这样运转的：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&lt;think&gt;
</span></span><span class="line"><span class="cl">INTENT: create a job
</span></span><span class="line"><span class="cl">HAVE: customer query &#34;Smith&#34;
</span></span><span class="line"><span class="cl">NEED: exact customer_id
</span></span><span class="line"><span class="cl">DECISION: search first
</span></span><span class="line"><span class="cl">&lt;/think&gt;
</span></span><span class="line"><span class="cl">&lt;action&gt;{&#34;type&#34;:&#34;search_customers&#34;,&#34;query&#34;:&#34;Smith&#34;}&lt;/action&gt;</span></span></code></pre></div></div>
<p><code>&lt;think&gt;</code> 标签在进 UI 之前会被剥掉，开发阶段我会留着它们调试。效果非常明显：模型会先把逻辑捋顺，再决定动手。</p>

<h2 class="relative group">把长对话塞进 8K 上下文
    <div id="把长对话塞进-8k-上下文" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%8a%8a%e9%95%bf%e5%af%b9%e8%af%9d%e5%a1%9e%e8%bf%9b-8k-%e4%b8%8a%e4%b8%8b%e6%96%87" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>工具结果一多，8K 上下文窗口比想象中满得快。我这边大约 30 轮就顶到上限了。</p>
<p>我的办法是对话压缩，让模型自己总结自己：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">compactPrompt</span> <span class="o">=</span> <span class="sb">`Summarize this conversation in under 200 words.
</span></span></span><span class="line"><span class="cl"><span class="sb">Include: user&#39;s goal, customers/jobs created (with IDs), and any unfinished tasks.
</span></span></span><span class="line"><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="cl"><span class="si">${</span><span class="nx">conversationHistory</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">m</span> <span class="o">=&gt;</span> <span class="sb">`</span><span class="si">${</span><span class="nx">m</span><span class="p">.</span><span class="nx">role</span><span class="si">}</span><span class="sb">: </span><span class="si">${</span><span class="nx">m</span><span class="p">.</span><span class="nx">content</span><span class="si">}</span><span class="sb">`</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="s1">&#39;\n\n&#39;</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">summary</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">chat</span><span class="p">([{</span> <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="nx">content</span>: <span class="kt">compactPrompt</span> <span class="p">}]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Replace history with summary
</span></span></span><span class="line"><span class="cl"><span class="nx">conversationHistory</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span> <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="nx">content</span><span class="o">:</span> <span class="sb">`[CONTEXT SUMMARY]</span><span class="err">\</span><span class="sb">n</span><span class="si">${</span><span class="nx">summary</span><span class="si">}</span><span class="err">\</span><span class="sb">n[END SUMMARY]`</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">];</span></span></span></code></pre></div></div>
<p>50 条消息的对话能压到 150 个 token 左右，模型接着聊，也不会忘记之前建了哪些工作单、客户 ID 是多少。</p>

<h2 class="relative group">会话保存
    <div id="会话保存" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bc%9a%e8%af%9d%e4%bf%9d%e5%ad%98" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>聊天会话持久化到我的 Django 后端：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">SessionMessage</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span> <span class="o">|</span> <span class="s2">&#34;assistant&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">content</span>: <span class="kt">string</span><span class="p">;</span>  <span class="c1">// Stripped for display
</span></span></span><span class="line"><span class="cl">  <span class="nx">raw</span>: <span class="kt">string</span><span class="p">;</span>      <span class="c1">// Full with &lt;think&gt; and &lt;action&gt; tags
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">api</span><span class="p">.</span><span class="nx">createAIChatSession</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">organization_id</span>: <span class="kt">orgId</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">title</span>: <span class="kt">firstUserMessage.slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">60</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">messages</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="nx">content</span>: <span class="kt">userText</span><span class="p">,</span> <span class="nx">raw</span>: <span class="kt">userText</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;assistant&#34;</span><span class="p">,</span> <span class="nx">content</span>: <span class="kt">cleanedResponse</span><span class="p">,</span> <span class="nx">raw</span>: <span class="kt">fullModelOutput</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p><code>content</code> 和 <code>raw</code> 两份都存的好处是：聊天记录在界面上回放时干干净净，恢复会话时又能拿回包含 action 在内的完整上下文。用户可以像用 ChatGPT 那样，在历史下拉菜单里切换对话。</p>

<h2 class="relative group">生产环境的性能数据
    <div id="生产环境的性能数据" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%94%9f%e4%ba%a7%e7%8e%af%e5%a2%83%e7%9a%84%e6%80%a7%e8%83%bd%e6%95%b0%e6%8d%ae" aria-label="锚点">#</a>
    </span>
    
</h2>
<p><strong>iPhone 14 Pro</strong>（A16 Bionic，6GB RAM）：</p>
<ul>
<li>模型加载：约 3 秒</li>
<li>首个 token：180-220ms</li>
<li>流式输出：14-16 tokens/秒</li>
<li>内存：约 1.8 GB</li>
</ul>
<p><strong>Samsung Galaxy S23</strong>（Snapdragon 8 Gen 2，8GB RAM）：</p>
<ul>
<li>模型加载：约 4 秒</li>
<li>首个 token：250-300ms</li>
<li>流式输出：10-12 tokens/秒</li>
<li>内存：约 2.1 GB</li>
</ul>
<p><strong>iPhone 11</strong>（A13，4GB RAM）：</p>
<ul>
<li>模型加载：约 6 秒</li>
<li>首个 token：400-500ms</li>
<li>流式输出：6-8 tokens/秒</li>
<li>内存：约 2.2 GB（内存吃紧时偶尔崩溃）</li>
</ul>
<p>我的经验值：想用得顺畅，RAM 至少 6GB。4GB 也能跑，但应用退到后台时多半得把模型卸载掉。</p>

<h2 class="relative group">内存管理
    <div id="内存管理" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%86%85%e5%ad%98%e7%ae%a1%e7%90%86" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>应用占着太多内存，iOS 会毫不客气地把它杀掉，所以一离开前台我就释放模型：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">subscription</span> <span class="o">=</span> <span class="nx">AppState</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">&#34;change&#34;</span><span class="p">,</span> <span class="p">(</span><span class="nx">nextAppState</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">nextAppState</span> <span class="o">===</span> <span class="s2">&#34;background&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">llamaContext</span><span class="o">?</span><span class="p">.</span><span class="nx">release</span><span class="p">();</span> <span class="c1">// Free ~2 GB
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">nextAppState</span> <span class="o">===</span> <span class="s2">&#34;active&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">loadModel</span><span class="p">();</span> <span class="c1">// Reload when foregrounded
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">subscription</span><span class="p">.</span><span class="nx">remove</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">},</span> <span class="p">[]);</span></span></span></code></pre></div></div>

<h2 class="relative group">电量消耗
    <div id="电量消耗" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%94%b5%e9%87%8f%e6%b6%88%e8%80%97" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>端侧推理在电量上不是免费的。实测数据：</p>
<table>
	<thead>
			<tr>
					<th>使用模式</th>
					<th>额外电池消耗</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>轻度（每天 5-10 次查询）</td>
					<td>每天 &lt;1%</td>
			</tr>
			<tr>
					<td>中度（每天 20-30 次查询）</td>
					<td>每天约 3-4%</td>
			</tr>
			<tr>
					<td>重度（每天 50+ 次查询）</td>
					<td>每天约 6-8%</td>
			</tr>
	</tbody>
</table>
<p>GPU 推理比 CPU 费电，但快得多。用户明显更喜欢秒回，所以我保留了 GPU。</p>

<h2 class="relative group">上线两个月，约 200 名测试用户
    <div id="上线两个月约-200-名测试用户" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%8a%e7%ba%bf%e4%b8%a4%e4%b8%aa%e6%9c%88%e7%ba%a6-200-%e5%90%8d%e6%b5%8b%e8%af%95%e7%94%a8%e6%88%b7" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>查询的分布有点出乎意料：</p>
<ol>
<li>&ldquo;Create a job for [customer name]&rdquo; - 占查询的 68%</li>
<li>&ldquo;Find jobs for [customer]&rdquo; - 15%</li>
<li>&ldquo;Update job [ID] to [status]&rdquo; - 9%</li>
<li>关于应用本身的问题 - 8%</li>
</ol>
<p>91% 的查询一次就成功。失败的原因分三类：用户把客户名字拼错导致搜不到（4%），超长对话撑爆上下文（3%），模型自己编客户 ID（2%）。</p>
<p>最后这个编 ID 的毛病以前严重得多。后来我把工具结果从叙述句改成 <code>customer_id=42</code> 这样的显式字段，问题基本消失了。小模型就是需要把饭喂到这个程度。</p>

<h2 class="relative group">成本账
    <div id="成本账" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%88%90%e6%9c%ac%e8%b4%a6" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>端侧方案：基础设施 $0/月，除了首次那 1.1 GB 下载，每个用户的成本是 $0，用户再多也是这个数。</p>
<p>云端方案粗算一下：平均一次对话约 2,000 个 token，200 个用户每月 30 次对话就是 6,000 次，也就是每月 1200 万 token。GPT-3.5 大约 $18/月，GPT-4 是 $360/月，Claude 是 $180/月。这个规模不至于伤筋动骨，但端侧方案第一天就回了本，哪怕涨到一万用户，还是 $0。</p>

<h2 class="relative group">得心里有数的几个限制
    <div id="得心里有数的几个限制" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%be%97%e5%bf%83%e9%87%8c%e6%9c%89%e6%95%b0%e7%9a%84%e5%87%a0%e4%b8%aa%e9%99%90%e5%88%b6" aria-label="锚点">#</a>
    </span>
    
</h2>
<p><strong>模型能力。</strong> Qwen 1.7B 擅长结构化任务，但多跳的复杂推理、事实性知识（它不是搜索引擎）、创意写作、语言里的微妙之处，它都不行。围绕小模型擅长的事来设计功能，就不会失望。</p>
<p><strong>设备要求。</strong> 底线是 3 GB RAM 加 2 GB 可用存储；实际上你需要 6 GB RAM、64 位处理器和 GPU 支持。iPhone 8、Galaxy S9 这类老设备很吃力，记得做特性检测和体面的降级。</p>
<p><strong>模型更新。</strong> 换模型意味着用户要重新下载 1.1 GB。我把版本号写进了存储路径：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">MODEL_PATH</span> <span class="o">=</span> <span class="sb">`</span><span class="si">${</span><span class="nx">FileSystem</span><span class="p">.</span><span class="nx">documentDirectory</span><span class="si">}</span><span class="sb">llama-models/v2/Qwen3-1.7B-Q4_K_M.gguf`</span><span class="p">;</span></span></span></code></pre></div></div>
<p>发新模型时把路径版本号加一就行，旧模型在应用卸载时自动清掉。</p>

<h2 class="relative group">iOS 和 Android 的差异
    <div id="ios-和-android-的差异" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ios-%e5%92%8c-android-%e7%9a%84%e5%b7%ae%e5%bc%82" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>走 Metal 的 iOS 比 Android 快 20% 左右，内存管理也更好，模型默认会备份到 iCloud（不想占用户备份空间的话，用 <code>NSURLIsExcludedFromBackupKey</code> 排除掉）。走 Vulkan/OpenCL 的 Android 机型间差异大得多，有些老 GPU 干脆不支持 Vulkan 只能回退到 CPU，存储也不会自动备份。两个平台都得测。好安卓机和差安卓机之间的差距，比两个平台之间的差距还大。</p>

<h2 class="relative group">调试技巧
    <div id="调试技巧" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b0%83%e8%af%95%e6%8a%80%e5%b7%a7" aria-label="锚点">#</a>
    </span>
    
</h2>
<p><strong>1. 打开详细日志</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">initLlama</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">model</span>: <span class="kt">MODEL_PATH</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">n_ctx</span>: <span class="kt">8192</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">n_gpu_layers</span>: <span class="kt">99</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">verbose</span>: <span class="kt">true</span><span class="p">,</span> <span class="c1">// Logs every token + timings
</span></span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p><strong>2. 跟踪 token 数量</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">tokens</span> <span class="o">=</span> <span class="nx">fullResponse</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="sr">/\s+/</span><span class="p">).</span><span class="nx">length</span> <span class="o">*</span> <span class="mf">1.3</span><span class="p">;</span> <span class="c1">// Rough estimate
</span></span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Generated </span><span class="si">${</span><span class="nx">tokens</span><span class="si">}</span><span class="sb"> tokens in </span><span class="si">${</span><span class="nx">duration</span><span class="si">}</span><span class="sb">ms`</span><span class="p">);</span></span></span></code></pre></div></div>
<p><strong>3. 监控内存</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">MemoryInfo</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;react-native-device-info&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">memoryUsage</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">MemoryInfo</span><span class="p">.</span><span class="nx">getUsedMemory</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Memory: </span><span class="si">${</span><span class="p">(</span><span class="nx">memoryUsage</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="p">).</span><span class="nx">toFixed</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="si">}</span><span class="sb"> MB`</span><span class="p">);</span></span></span></code></pre></div></div>
<p><strong>4. 离线测试</strong></p>
<p>开飞行模式，认真把应用用一遍。模型应该从缓存加载，推理应该完整跑完，唯一失败的应该只有后端持久化，而且要败得体面。</p>

<h2 class="relative group">接下来想做的
    <div id="接下来想做的" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%8e%a5%e4%b8%8b%e6%9d%a5%e6%83%b3%e5%81%9a%e7%9a%84" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>llama.cpp 已经支持视觉模型（LLaVA、Qwen2-VL），&ldquo;这是窗帘面料的照片，加到工作单里&quot;这种用法跟这个应用简直是天作之合。我还想拿过去的工作单描述和客户对话微调一个 LoRA 适配器，估计领域内查询的准确率能涨 20-30%。再远一点：配上 Whisper.cpp 做完全离线的语音助手，以及在原始数据不出手机的前提下，用联邦学习让模型在所有用户间共同进步。</p>

<h2 class="relative group">值不值得做？
    <div id="值不值得做" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%80%bc%e4%b8%8d%e5%80%bc%e5%be%97%e5%81%9a" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>如果你在做处理敏感数据的业务应用，走离线优先的工作流，主要任务是分类、录入、搜索这类结构化工作，尤其是担心规模上去之后的 API 账单，那答案是值得，今天就能动手。</p>
<p>如果你要的是开放式聊天，用 Claude 或 GPT-4。知识密集型任务（小模型真的没什么见识）、必须支持低端设备、领域知识变化快过你更新模型的速度，这几种情况也一样。</p>
<p>整个实现花了我 12 个小时左右。每月运行成本 $0，飞机上照常工作，客户数据从头到尾没离开过客户的手机。挑不出毛病。</p>
<hr>
<p><em>有问题或想聊聊？我是 <a href="https://twitter.com/jaredlynskey"  target="_blank" rel="noreferrer">@jaredlynskey</a>。llama.rn 库在 <a href="https://github.com/mybigday/llama.rn"  target="_blank" rel="noreferrer">github.com/mybigday/llama.rn</a>。</em></p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-01-30-on-device-ai/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
