


[{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/authors/aaron/","section":"Authors","summary":"","title":"Aaron","type":"authors"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/tags/agent/","section":"标签","summary":"","title":"Agent","type":"tags"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/tags/ai/","section":"标签","summary":"","title":"AI","type":"tags"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/tags/harness-engineering/","section":"标签","summary":"","title":"Harness Engineering","type":"tags"},{"content":" 前言 # 最近 Harness Engineering 这个概念在 AI 圈火得不行，各路技术博客和播客都在讨论。我花了不少时间研究相关资料，包括 Anthropic 和 OpenAI 的公开实践，越看越觉得这东西确实是 Agent 落地过程中绕不过去的一环。很多人在做 Agent 的时候都有过类似的困惑：模型不差、提示词也改了好几版，但跑起来就是不稳定。问题的根源往往不在模型本身，而在模型外面那套运行系统。这套东西，现在有了一个统一的名字：Harness。这篇文章把我研究的心得整理出来，聊聊 Harness Engineering 到底是什么，包含哪些核心部分。\n三次重心转移 # 过去两年，AI 工程经历了三次明显的重心转移：从 Prompt Engineering 到 Context Engineering，再到最近的 Harness Engineering。表面上看是换了几个新名词，但它们分别对应了 AI 系统发展中的三个核心问题：\n模型有没有听懂你在说什么？ 模型有没有拿到足够且正确的信息？ 模型在真实执行中能不能持续做对？ 这些问题是一层一层往外扩张的。\nPrompt Engineering：把话说清楚 # 大模型刚火起来的时候，大家最直观的感受是：同一个模型，换一种说法，结果可能差很多。说「帮我总结一下这篇文章」得到一个平庸的摘要，换一种更结构化的表述，效果马上就不一样。\n于是大家开始疯狂研究提示词：角色设定、风格约束、Few-shot 示例、分步引导、输出格式等等。为什么这些东西有效？因为大模型本质上是一个对上下文非常敏感的概率生成系统。你给它什么身份，它就沿着那个身份回答；你给它什么样的示例，它就沿着那个范式补全；你强调什么约束，它就把那部分当成重点1。\n所以 Prompt Engineering 的本质不是命令模型，而是塑造一个局部的概率空间。这个阶段最重要的能力是语言的设计。\n但 Prompt Engineering 很快就遇到了天花板。很多任务不是你说清楚就行，而是真的需要信息支撑。比如让模型分析一份公司的内部文档、回答产品的最新配置、按规范写代码、在多个工具之间完成复杂任务。提示词写得再漂亮，也替代不了事实本身。\nPrompt 擅长的：约束输出、激发模型已有能力、短链路任务。不擅长的：弥补缺失的知识、管理大量动态信息、处理长链路任务里的状态。\nContext Engineering：把信息给对 # 当 Agent 开始兴起，模型不再只是回答问题，而是要进入真实环境做事情。它需要多轮对话、调浏览器、读写代码、操作数据库，还要在多个步骤之间传递中间结果，根据反馈不断修正计划。\n这时候系统面对的已经不是「一次回答对不对」，而是「整条链路的任务能不能跑通」。以一个真实任务为例：「分析这份需求文档，找出潜在风险，结合历史评审意见给出改进建议，再生成一版发给产品经理的反馈稿」。这已经完全不是一句提示词能解决的了。它至少需要拿到当前需求文档、历史评审记录、相关规范、当前目标、已分析的中间结论、输出的对象、语气要求等等。\nContext Engineering 的核心就变成了一句话：模型未必知道，系统必须在合适的时机把正确的信息送进去。\n这里的 Context 不只是几段背景资料。在工程意义上，它代表了所有影响模型当前决策的信息总和：用户输入、历史对话、检索结果、工具返回、任务状态、中间产物、系统规则、安全约束，以及其他 Agent 传过来的结构化结果。Prompt 其实只是 Context 的一部分2。\nRAG 算是一个典型的 Context Engineering 实践。但真正成熟的上下文工程关注的远不只是检索：文档怎么切块、结果怎么排序、长文怎么压缩、历史对话什么时候保留什么时候摘要、工具返回要不要全部塞给模型、多个 Agent 之间传原文还是摘要还是结构化字段。这些都需要精心设计。\nAgent Skill 的渐进式披露也是上下文工程的高级实践。它解决了一个现实问题：如果把十几个工具的说明、参数定义全部塞给模型，理论上它知道得更多，但实践效果往往更差。因为上下文窗口是稀缺资源，信息一多注意力就涣散。Skill 采用的策略是只给模型看最少量的元信息，等它真正需要触发某些能力时，再动态加载详细的参考信息和脚本。\n这个思路很关键：上下文的优化不是给得更多，而是按需给、分层给、在正确的时机给。\nHarness Engineering：让模型持续做对 # 但 Context Engineering 也不是终点。就算信息给对了，模型也不一定能稳定执行。它可能计划做得很好但执行跑偏了，调了工具但理解错了返回结果，在一个很长的链路里慢慢偏航而系统却没发现。\nPrompt 优化意图表达，Context 优化信息供给，但复杂任务里还有一个更难的问题：当模型开始连续行动时，谁来监督它、约束它、纠偏它？\n这就是 Harness Engineering 要解决的。\nHarness 是什么 # Harness 这个词原本是挽具、绳索、约束装置的意思。放在 AI 系统里，它提醒我们一件朴素的事情：当模型从回答问题走向执行任务，系统不仅要喂信息，还要驾驭整个过程。\n有人给过一个很简洁的公式：Agent = Model + Harness。也就是说，在一个 Agent 系统里，除了模型本身以外，几乎所有能决定它能不能稳定交付的东西，都属于 Harness 的范畴。\n打个比方。假设你要派一个新人去完成一次重要的客户拜访。\nPrompt 做的事，相当于把任务讲清楚：见面先寒暄、再介绍方案、再问需求、最后确认下一步。重点是把话说对 Context 做的事，相当于把资料准备齐全：客户背景、过往沟通记录、产品报价、竞品情况、这次会议的目标。重点是把信息给对 Harness 做的事，相当于建立完整的运行保障：让他带着 Checklist 去，关键节点实时汇报，会后核实纪要和录音，发现偏差马上纠正，按明确标准验收结果。重点是有没有一套持续观测、纠偏和验收的机制 三者不是替代关系，而是包含关系：Prompt 是对指令的工程化，Context 是对输入环境的工程化，Harness 是对整个运行系统的工程化。边界一层比一层大。前两代工程关注的是「怎么让模型更会想」，Harness 关注的则是「怎么让模型别跑偏、跑得稳、出了错还能拉回来」。\nHarness 的六层结构 # 把一个成熟的 Harness 拆开来看，我认为可以分成六层。\n第一层：上下文管理 # 模型能不能稳定发挥，很多时候不取决于它聪不聪明，而取决于它看到了什么。Harness 的第一层职责是让模型在正确的信息边界内思考，通常包括三件事：\n角色和目标定义：模型要知道自己是谁、任务是什么、成功的标准是什么 信息裁剪和选择：上下文不是越多越好，而是越相关越好 结构化组织：固定规则、当前任务、运行状态、外部证据分层放好。信息一乱，模型就容易漏重点、忘约束，甚至自我污染 说到信息裁剪，OpenAI 踩过一个很典型的坑：他们早期写了一个巨大的 Agent 指令文档，把所有规范、框架、约定全部塞进去，结果 Agent 反而更糊涂了。上下文窗口是稀缺资源，塞得太满等于什么都没说。后来他们把文档改成了目录页，只保留核心索引，详细内容拆到架构文档、设计文档、执行计划、质量评分、安全规则等子文档里。Agent 先看目录，需要时再钻进去。这和 Agent Skill 的渐进式披露是同一个思路：不是一次性全给，而是按需暴露。\nAnthropic 在长时间自主任务中也遇到了类似问题。上下文越来越满，模型开始丢细节、丢重点，甚至出现一种有意思的现象：模型好像知道自己快装不下了，于是着急收尾。常见的做法是上下文压缩，但 Anthropic 发现对某些场景来说光压缩还不够，压缩只是变短了，不代表负担感真的消失。所以他们做了一件更激进的事：Context Reset。不是在原上下文里继续压，而是换一个干净的新 Agent 接手工作。这个思路很像工程里遇到内存泄漏，不是清缓存，而是直接重启进程再恢复状态3。\n第二层：工具系统 # 没有工具，大模型本质上还是一个文本预测器。连上工具后，模型才可以真正做事：搜网页、读文档、写代码、调 API。\n但 Harness 在这里做的不是简单地把工具挂上去，而是要解决三个问题：\n给什么工具：太少能力不够，太多模型会乱用 什么时候调用：不需要查的时候别乱查，该查的时候别硬答 工具结果怎么回喂：搜索返回的几十条结果不应该原封不动塞回去，要提炼筛选，保持和任务的相关性 OpenAI 在这方面的实践比较极端：他们不只给 Agent 接代码编辑器，还接上浏览器让 Agent 能截图、模拟用户操作，接上日志和指标系统让 Agent 能查 Log、查监控，每个任务都在独立隔离的环境里跑。Agent 不再是写完代码就说「写完了」，而是能真正跑起来、看结果、发现 Bug、修 Bug、再验证。工具系统的设计直接决定了 Agent 能做到多「真实」。\n第三层：执行编排 # 这一层解决的核心问题是：模型下一步该做什么。\n很多 Agent 的问题不是某一步不会，而是不会把所有步骤串起来。它会搜索、会总结、会写代码，但整个过程想到哪做到哪，最后交付一堆半成品。一个完整的任务通常需要这样的轨道：理解目标 → 判断信息够不够（不够就补）→ 基于结果分析 → 生成输出 → 检查输出 → 不满足要求就修正或重试。\nOpenAI 在这方面提出过一个很有启发性的观点：工程师在 Agent 时代的工作不再是写代码，而是设计环境。具体来说就是三件事：把产品目标拆解成 Agent 能力范围内的小任务；Agent 失败时不是让它更努力，而是问环境里缺了什么能力；建立反馈链路让 Agent 能看到自己的工作结果。「Agent 出了问题，修复方案几乎从来不是让它更努力，而是确定它缺了什么能力」。这本身就是典型的 Harness 思维。\n第四层：记忆和状态 # 没有状态的 Agent 每一轮都像失忆一样。它不知道自己刚做了什么，哪些结论已经确认，哪些问题还没解决。Harness 必须管理状态，至少要分清三类东西：\n当前任务的状态 会话中的中间结果 长期记忆和用户偏好 三类信息如果混在一起，系统会越来越乱。分清楚之后，Agent 才像一个稳定的协作者。\n第五层：评估和观测 # 这是很多团队最容易忽视的一层。很多系统不是生成不出来，而是生成完了之后根本不知道自己做得好不好。没有独立的评估能力，Agent 就会长期停留在「自我感觉良好」的状态。\n这一层通常包括：输出验收、环境验证、自动测试、日志和指标、错误归因。系统不仅要会做，还要知道自己有没有真的做对。\n这里有一个关键的工程原则：生产和验收必须分离。模型自己干活再自己打分，往往偏乐观，尤其在设计体验、产品完整度这类没有标准答案的问题上偏差更明显。Anthropic 的做法是把角色拆开：Planner 负责把模糊需求展开成完整规格，Generator 负责逐步实现，Evaluator 像 QA 一样真实测试。关键是 Evaluator 不只是看代码，而是真实操作页面、检查具体交互和实际结果。只要评估者足够独立，系统就能形成「生成 → 检查 → 修复 → 再检查」的有效循环。\n第六层：约束、校验与失败恢复 # 最后一层才是真正决定系统能不能上线的关键。因为在真实环境里，失败不是例外而是常态。搜索不准、API 超时、文档格式混乱、模型误解任务，这些都是家常便饭。\n如果没有恢复机制，Agent 每次出错就只能从头再来。一个成熟的 Harness 必须包括：\n约束：哪些能做，哪些不能做 校验：输出前、输出后怎么检查 恢复：失败后怎么重试、切路径、回滚到稳定状态 OpenAI 在这一层做了一个很值得借鉴的事：把资深工程师的经验直接编码为系统规则。模块怎么分层、哪一层不能依赖哪一层、什么情况下必须拦截、发现问题后怎么修。这些规则不只是报错，而是把修复方案一起反馈给 Agent，进入下一轮上下文。这已经不是传统意义上的代码规范，而是一套可持续运行的自动治理系统4。Agent 提交代码的速度远超人类 Code Review 的处理能力，所以必须靠系统规则来兜底，而不是依赖人工审查。\n总结 # 把整条线索串起来：\nPrompt Engineering 解决怎么把任务讲清楚 Context Engineering 解决怎么把信息给对 Harness Engineering 解决怎么让模型在真实执行中持续做对 Harness 不是在取代前两者，而是在更大的系统边界上把它们包含进来。当任务是简单的单轮生成时，Prompt 很重要；当任务依赖外部知识和动态信息时，Context 就很关键；当模型进入长链路、可执行、低容错的真实场景时，Harness 几乎不可避免。\n这也是为什么同样的模型在不同产品里表现差距会这么大。真正决定能不能上线的是模型，但真正决定能不能落地、能不能稳定交付的，是 Harness。AI 落地的核心挑战，正在从「让模型看起来更聪明」转向「让模型在真实世界里稳定工作」。\n这种对上下文的敏感性既是大模型的优势也是弱点。优势在于我们可以通过精心设计输入来引导输出，弱点在于细微的措辞变化可能导致截然不同的结果。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n广义上，Prompt 也是一种 Context。但在工程实践中，我们通常把用户直接编写的指令称为 Prompt，把系统自动组织和注入的信息称为 Context，以区分两者的管理方式。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nContext Reset 的本质是一种更激进的上下文管理策略。与压缩不同，它放弃了在原有上下文上继续工作的思路，转而通过状态序列化 + 新 Agent 加载的方式重启整个推理过程。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n这种「把工程师经验编码为系统规则」的思路，和传统软件工程中的 Lint 规则、CI 检查有异曲同工之妙，只是执行者从人变成了 Agent。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2026-03-23","externalUrl":null,"permalink":"/posts/2026/03/harness-engineering-secret/","section":"所有文章","summary":"","title":"Harness Engineering：AI Agent 稳定运行的秘密","type":"posts"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/","section":"Hideaway","summary":"","title":"Hideaway","type":"page"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/tags/prompt-engineering/","section":"标签","summary":"","title":"Prompt Engineering","type":"tags"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/en/categories/tech-reflections/","section":"Categories","summary":"","title":"Tech Reflections","type":"categories"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/tags/","section":"标签","summary":"","title":"标签","type":"tags"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/categories/","section":"分类","summary":"","title":"分类","type":"categories"},{"content":"","date":"2026-03-23","externalUrl":null,"permalink":"/categories/%E6%8A%80%E6%9C%AF%E6%80%9D%E8%80%83/","section":"分类","summary":"","title":"技术思考","type":"categories"},{"content":"您可以通过 RSS 订阅所有博客文章\n","date":"2026-03-23","externalUrl":null,"permalink":"/posts/","section":"所有文章","summary":"","title":"所有文章","type":"posts"},{"content":"","date":"2026-02-22","externalUrl":null,"permalink":"/tags/llm/","section":"标签","summary":"","title":"LLM","type":"tags"},{"content":"","date":"2026-02-22","externalUrl":null,"permalink":"/tags/mcp/","section":"标签","summary":"","title":"MCP","type":"tags"},{"content":"","date":"2026-02-22","externalUrl":null,"permalink":"/en/categories/tech-notes/","section":"Categories","summary":"","title":"Tech Notes","type":"categories"},{"content":"","date":"2026-02-22","externalUrl":null,"permalink":"/tags/token/","section":"标签","summary":"","title":"Token","type":"tags"},{"content":"","date":"2026-02-22","externalUrl":null,"permalink":"/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/","section":"分类","summary":"","title":"技术笔记","type":"categories"},{"content":" 前言 # 前段时间系统性地梳理了一遍 AI 相关的技术概念，发现很多东西之前只是零散地接触过，真要把它们之间的依赖关系说清楚，还真得花点功夫。整理完之后最大的感受是：这些概念不是孤立存在的，而是一层一层堆叠起来的，每一层都在补上一层的短板。这篇文章是我梳理过程的记录，试着从最底层一路往上捋，把整条链路串通。\nLLM：一切的起点 # LLM 全称 Large Language Model，大语言模型，通常简称大模型。目前主流的大模型都基于 Transformer 架构1，这套架构最早由 Google 团队在 2017 年的论文 Attention Is All You Need 中提出。有意思的是，Google 虽然点燃了火种，但真正把它引爆的却是 OpenAI：2022 年底 GPT-3.5 横空出世，几个月后 GPT-4 发布，直接把 AI 能力的天花板拉到了新高度。到今天，GPT 家族仍然是行业标杆之一，不过 Claude、Gemini 等后来者也在各自擅长的领域强势竞争。\n大模型的工作原理其实很朴素：本质上就是一个文字接龙游戏。你给它一段输入，它预测下一个概率最高的词，然后把这个词追加到输入里，再预测下一个，如此循环，直到输出一个特殊的结束标识符。\n这也是为什么你在 ChatGPT 里看到回答总是一个字一个字地蹦出来，不是因为网速慢，而是它的工作机制就是逐词生成。\nToken：大模型的最小处理单元 # 前面说大模型做「文字接龙」，但这个说法其实是个简化。大模型本质上是庞大的数学函数，内部全是矩阵运算，它只认识数字，不认识文字。人和模型之间需要一个翻译层，这个翻译层就是 Tokenizer。\nTokenizer 的工作分两步：\n切分：把输入文本拆成最小的片段，每个片段就是一个 Token 映射：把每个 Token 映射到一个数字（Token ID），因为模型只认数字 比如一句话「今天天气真好」，Tokenizer 可能会把它切成「今天」「天气」「真」「好」四个 Token，再各自映射成不同的数字。模型输出的数字，也会经过反向映射还原成文字。\n但 Token 不等于我们日常理解的「词」。比如「人工智能」可能被切成「人工」和「智能」两个 Token，英文的「unbelievable」可能被切成「un」「believ」「able」三段。某些特殊字符甚至需要更多 Token 才能表示。Token 是模型在训练过程中自己学会的一套切分规则，跟自然语言的词并没有严格的对应关系。\n粗略估算，一个 Token 大约等于 0.75 个英文单词，或者 1.5 到 2 个汉字。40 万 Token 大概就是 60 到 80 万汉字2。\nContext 与 Context Window # 你可能好奇过：大模型为什么能「记住」你之前说的话？它又没有真正的记忆。\n秘密在于：每次你发消息时，背后的程序会把之前的完整对话历史连同你的新问题一起发给模型。模型每次看到的都是全部内容，所以它才知道之前发生了什么。\n这就引出了 Context（上下文）的概念。它代表大模型每次处理任务时接收到的信息总和，包括用户问题、对话历史、系统指令、工具列表等。你可以把它理解为大模型的临时记忆体。\n那这个记忆体能有多大？这就是 Context Window（上下文窗口）的范畴了。它规定了 Context 能容纳的最大 Token 数量。目前主流大模型的 Context Window 已经相当可观：GPT-5.4 是 105 万 Token，Gemini 3.1 Pro 和 Claude Opus 4.6 都是 100 万 Token。100 万 Token 约等于 150 万汉字，整个《哈利波特》全集都能装下。\n不过 Context Window 不是万能的。假如你有一份上千页的产品手册想让大模型据此回答用户问题，把全文塞进去虽然技术上可行，但成本会失控。这种场景更适合用 RAG（Retrieval-Augmented Generation）技术3，先从手册中检索出与用户问题最匹配的片段，只把这些片段发给模型，既不受 Context Window 限制，成本也低得多。\nPrompt：给模型的指令 # Prompt（提示词）就是你给大模型的具体问题或指令。「帮我写一首诗」就是一个 Prompt。听起来简单，但 Prompt 的写法直接决定了输出质量。说「帮我写一首诗」，模型可能给你古诗、现代诗甚至打油诗，因为它不知道你具体想要什么。改成「写一首五言绝句，主题是秋天的落叶，风格悲凉一些」，效果就好得多。\n这就是所谓 Prompt Engineering（提示词工程）。这个领域曾经很火，现在提的人不多了，一是因为门槛低（本质就是把话说清楚），二是因为模型越来越聪明，即使提示词含糊也能大致猜到你的意图。\nPrompt 分两种：\nUser Prompt（用户提示词）：你在对话框里直接输入的内容 System Prompt（系统提示词）：开发者在后台配置的人设和行为规则，用户看不到，但会持续影响模型的行为 举个例子。你在做一个客服机器人，不希望它随便承诺补偿或退款。于是在 System Prompt 里写「你是某电商平台的售后客服，遇到用户投诉时，先安抚情绪，再了解具体问题，不得自行承诺退款或赔偿，需要升级处理的引导用户填写工单」。用户输入「我收到的商品碎了，我要退款」，模型就会回答「很抱歉给您带来不好的体验，能拍一张破损照片发给我吗？确认情况后我会帮您尽快处理」而不是直接说「好的，马上退」。\nTool：让模型感知外部世界 # 大模型有一个明显的短板：它无法感知外界环境。问它「今天北京天气怎么样」，它会老老实实告诉你「我无法获取实时信息」，因为它的知识停留在训练数据截止的那一刻。\nTool（工具）就是用来弥补这个短板的。Tool 本质上就是一个函数，给它输入，它就给你输出。比如一个天气查询工具，接收城市和日期参数，内部调用气象局接口，返回天气信息。\n关键在于：大模型无法自己调用工具。它唯一的能力就是输出文本。所以整个流程需要一个中间角色（通常称为平台）来串联：\n用户的问题和可用工具列表一起发给模型 模型分析后输出一条工具调用指令（包含工具名称和参数） 平台收到指令，实际调用对应的工具函数 工具返回结果，平台把结果转发给模型 模型把结果整理成自然语言回复给用户 每个角色各司其职：模型负责选择工具和归纳总结，工具负责执行具体操作，平台负责串联整个流程。\nMCP：统一工具接入标准 # Tool 解决了能力问题，但带来了一个新的工程问题：接入标准不统一。\n给 ChatGPT 做工具接入，你得按 OpenAI 的规范写一套代码；给 Claude 做接入，按 Anthropic 的规范再写一套；给 Gemini 做接入，又得按 Google 的规范写第三套。同一个工具要写三遍，因为每家的接入格式都不一样。\nMCP（Model Context Protocol，模型上下文协议）4就是为了解决这个问题而生的。它定义了一套统一的工具接入标准，开发者只需按 MCP 规范开发一次，工具就能被所有支持 MCP 的平台使用。类比一下，就像所有手机都统一用 Type-C 充电口，配件厂商不用再为每种手机单独做适配了。\nAgent：自主规划的智能体 # 有了 Tool 和 MCP，大模型已经能做不少事了。但面对更复杂的需求，单次工具调用就不够用了。\n比如用户说：「我下午要出门跑步，帮我看看适不适合。」这需要调用多次工具，而且后续调用依赖前一步的结果：先定位获取经纬度，再用经纬度查天气和空气质量，拿到结果后根据多项指标综合判断是否适合户外运动。整个过程需要模型一步步思考当前状态、决定下一步做什么。\n这种能够自主规划、自主调用工具、持续工作直到完成任务的系统，就是 Agent。目前市面上的 Claude Code、Codex、Gemini CLI 等产品，本质上都是 Agent。它们的构建模式各有不同，比较经典的有 ReAct、Plan and Execute 等5。\nAgent Skill：给 Agent 的操作手册 # Agent 听起来已经很强大了，但在高频使用中你会发现一个痛点：个性化规则每次都得重复输入。\n举个例子。你希望 Agent 做你的健身顾问，每次运动前帮你评估身体状况和当天的运动条件。你有自己的一套偏好：膝盖不好的时候不做深蹲、空气质量指数超过 150 改室内运动、温度超过 35 度减量、运动后必须提醒拉伸。而且你希望输出格式固定：先一个综合评分，再列具体建议。\n如果没有额外设定，Agent 虽然会查数据，但不知道你的身体状况和运动偏好，给出的建议大概率很泛泛。于是你每次提问时都得附上一大段要求，复制粘贴，非常反人类。\nAgent Skill 就是解决这个问题的。它本质上是你提前写好的一份说明文档（Markdown 格式），告诉 Agent 在特定场景下应该怎么做事。分为两层：\n元数据层：相当于封面，标明这个 Skill 的名称和描述（比如名称 Fitness Advisor，描述「运动前综合评估并给出建议」） 指令层：具体的操作规则，包括目标、执行步骤、判断规则、输出格式和示例 定义好之后存到指定位置。以 Claude Code 为例，放在 ~/.claude/skills/ 目录下，文件夹名必须和 Skill 名称一致，里面的文件必须命名为 SKILL.md。\n运行时，Agent 启动时会加载所有 Skill 的元数据。当用户问题与某个 Skill 的描述相关时，Agent 才会读取该 Skill 的完整指令层，然后按照里面的规则执行。这种渐进式披露机制（只在需要时才加载完整内容）能有效节省 Token6。\n全景回顾 # 把这些概念串起来，整个 AI 技术栈的层次关系就清晰了：\n概念 定位 解决的问题 LLM 基座 文本生成的基础能力 Token 数据单元 人与模型之间的文字/数字转换 Context 记忆体 让模型拥有临时记忆 Prompt 指令 告诉模型做什么、怎么做 Tool 能力扩展 让模型感知和影响外部环境 MCP 协议标准 统一工具接入格式，避免重复开发 Agent 自主系统 多步规划和工具调用，完成复杂任务 Agent Skill 定制化 让 Agent 按你的规则和格式做事 每一层都建立在前一层的基础上，解决前一层遗留的问题。理解了这个层次关系，再看 Claude Code、Codex、OpenClaw 这些产品，你会发现它们的本质都是在这个框架下运作的。技术名词再多，底层逻辑是相通的。\nTransformer 架构的细节不是本文的重点，想深入了解可以阅读原论文 Attention Is All You Need。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n不同模型的 Tokenizer 实现不同，Token 与文字的对应比例会有差异，这里的数字是大致估算。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nRAG 的核心思路是「先检索，再生成」，通过向量相似度匹配找到最相关的文档片段，只把这些片段作为 Context 发给模型。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nMCP 由 Anthropic 在 2024 年底发布，目前已经被多个平台和工具采纳。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n关于 Agent 构建模式的详细拆解，包括 ReAct 和 Plan and Execute 的运行流程，可以参考相关专题文章。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nAgent Skill 还有运行代码、引用外部资源等高级功能，这里只介绍了最核心的用法。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2026-02-22","externalUrl":null,"permalink":"/posts/2026/02/ai-eight-layers/","section":"所有文章","summary":"","title":"理解 AI 的八个层次：LLM、Token、Agent 和它们之间的联系","type":"posts"},{"content":"","date":"2026-01-07","externalUrl":null,"permalink":"/tags/claude-code/","section":"标签","summary":"","title":"Claude Code","type":"tags"},{"content":" 前言 # Claude Code 出来有一阵子了，身边不少朋友都试过，但大部分人还停留在「输入需求、等它写代码」的阶段。说真的，Claude Code 的能力远不止于此。想要真正把它用顺手、嵌入日常开发流程，有不少细节值得了解。这篇文章把我在实战中摸索出来的用法整理了一遍，从安装配置到高级定制，力求覆盖一条完整的链路。\n另外，像 Codex、OpenCode 这些同类产品，底层架构和使用方式跟 Claude Code 大同小异。把 Claude Code 摸透了，迁移到其他工具基本没什么门槛。\n安装与登录 # 安装就一行命令，去 Claude Code 官网复制安装脚本，在终端粘贴执行即可。\n安装完成后，在项目目录下运行 claude 命令启动。首次使用会提示登录，也可以手动执行 /login 触发。Claude Code 提供两种接入方式：\n订阅制：购买了 Claude Pro 或 Max 会员，直接授权使用，省心 API Key 按量计费：用多少花多少，适合轻量使用 如果你无法访问 Claude 官方服务，Claude Code 本身并不绑定特定模型。通过设置环境变量，可以接入 GLM、Minimax 等国产模型来驱动1。\n三种交互模式 # Claude Code 有三种工作模式，通过 Shift+Tab 循环切换。理解它们的区别，是用好 Claude Code 的基础。\n默认模式 # 启动后的初始模式。底栏只显示一行灰色提示 ? for shortcuts，没有额外的模式标签。这种「什么都不标注」的状态就是默认模式。\n在这个模式下，Claude Code 每次创建或修改文件前都会征求你的同意。三个选项分别是：\nYes：单次授权，仅同意当前这次操作 Yes, allow all edits during this session：本次会话期间，所有文件操作自动通过 No：拒绝，你可以继续输入想法 默认模式最为稳妥，适合对代码变更比较敏感的场景。\n自动模式（Accept Edits On） # 底栏显示 Accept Edits On 时表示处于自动模式。此时 Claude Code 会直接创建和修改文件，不再逐一询问。做需求迭代的时候效率很高，尤其是那种「让它一口气改十几个文件」的场景。\n规划模式（Plan Mode） # 底栏显示 Plan Mode On。这个模式只讨论方案，不写入任何文件。适合在动手之前理清思路、确定细节，后面会单独展开讲。\nPlan Mode：先想清楚再动手 # 假设你有一个用 HTML 写的小项目，想迁移到 React + TypeScript + Vite 的架构。这种涉及目录结构调整的大改动，直接让 Claude Code 开干容易翻车。更好的做法是先进入 Plan Mode，把方案聊清楚。\n操作流程是这样的：\n按 Shift+Tab 切到 Plan Mode 输入你的需求，比如「将当前项目重构为 React + TypeScript + Vite，保留所有现有功能，UI 风格保持一致」 这里有个小技巧：输入多行内容时，按 Shift+Enter 换行，直接按 Enter 会提交请求。如果你觉得终端的输入框不够好用，可以按 Ctrl+G 打开 VS Code 编辑器来写，保存关闭后内容会自动回到 Claude Code 的输入框里。\nClaude Code 会输出一份完整的计划，包含目标、文件清单、目录结构等。它还会给你三个选项：\n执行计划并进入自动模式：后续改文件不再询问 执行计划并保持默认模式：后续改文件仍需逐一确认 继续修改计划：对方案不满意，可以继续补充需求 比如你觉得计划里少了「给每个待办事项增加优先级标记（高/中/低），用不同颜色区分」这个需求，就选第三个选项补充进去。Claude Code 会重新生成一份修改后的计划。\n确认计划后选择执行，Claude Code 就开始按步骤实施了。\n一个容易踩的坑：自动模式只针对文件读写操作。终端命令（比如 mkdir、npm install）属于更敏感的操作，Claude Code 默认每次都会询问。如果你希望彻底跳过所有权限检查，可以在启动时加上 --dangerously-skip-permissions 参数。顾名思义，官方在参数名里就写了「dangerously」，意味着 Claude Code 将拥有完整的终端权限，不会征求任何确认。效率拉满，但风险自担。\n终端命令与后台任务 # 执行终端命令 # 在 Claude Code 的输入框里输入 ! 开头，就进入了 Bash 模式，可以运行任意终端命令。比如 ! ls 查看文件列表，! open index.html 在浏览器中打开页面。\n后台任务 # 有一个容易忽略的点：某些命令（比如 npm run dev 启动开发服务器）会阻塞 Claude Code。服务在跑的时候，Claude Code 无法响应新的请求。\n解决办法是按 Ctrl+B 把任务放到后台。之后你可以继续和 Claude Code 交互。用 /tasks 命令查看当前运行的后台任务，在任务列表里按 K 可以关掉指定的后台服务。\n回滚操作 # Claude Code 在你每次提交请求时都会自动创建一个回滚点。连按两下 Esc 就能进入回滚页面，选择一个回滚点后，有四种操作：\n回滚代码和会话：文件内容和对话记录都恢复 仅回滚会话：只恢复对话，文件不动 仅回滚代码：只恢复文件，对话保留 放弃回滚 回滚功能用起来很方便，但有一个限制：它只能回滚 Claude Code 自己写入的文件。通过终端命令创建的目录、安装的依赖包等，Claude Code 无法自动清理。如果你需要精准回滚，还是得靠 Git。\n图片输入与 Figma MCP # 直接传图片 # 想让 Claude Code 照着设计稿写页面？直接把图片拖进终端，或者复制图片后按 Ctrl+V 粘贴。注意，即使在 macOS 上也要用 Ctrl+V，Cmd+V 不起作用。\n传完图片后继续输入需求，Claude Code 就会参考图片来生成代码。不过说实话，纯靠图片做 UI 还原精度有限，字体大小、间距这些细节很难做到像素级准确。\n接入 Figma MCP # 如果设计稿在 Figma 上，有一个更精确的方案：接入 Figma 的 MCP Server2。\nMCP（Model Context Protocol）是大模型与外部工具通信的协议。通过 Figma MCP，Claude Code 不仅能拿到设计稿的截图，还能获取完整的组件间距、字体样式、颜色值等结构化信息。\n配置步骤：\n安装 Figma MCP Server（一行命令，官方文档有写） 重启 Claude Code，用 -c 参数（claude -c）恢复上次对话 执行 /mcp 命令，选择 Figma 工具进行授权 授权完成后，直接输入需求，比如「修改当前页面，使它与 Figma 稿件保持一致」，附上 Figma 的设计稿链接 Claude Code 会自动识别到 Figma MCP，调用 get_design_context 和 get_screenshot 等工具获取设计信息，然后根据这些结构化数据来修改代码。还原精度比单纯看图片高很多。\n上下文管理 # 对话进行到一定阶段，上下文里会堆积大量代码片段、工具调用结果等信息。这些信息中有用的和无用的混杂在一起，既影响模型性能，又浪费 Token。\n/compact：压缩上下文 # 执行 /compact 命令可以对上下文做智能压缩，Claude Code 会把冗余信息精简掉，保留核心内容。压缩完成后按 Ctrl+O 可以查看压缩结果。你会发现之前一大段对话内容被浓缩成了几行关键信息。\n你还可以在 /compact 后面附加具体的压缩策略，比如 /compact 重点保留用户提出的需求，让压缩结果更符合你的预期。\n不过压缩的可控性有限，你没法直接编辑压缩后的内容。\n/clear：清空上下文 # 比压缩更彻底。/clear 直接把所有上下文内容清空，适合前后任务完全无关的场景。\nCLAUDE.md：让 Claude Code 更懂你 # 无论是压缩还是清空，上下文始终和某次会话绑定。换个会话 Claude Code 就什么都不知道了。有没有办法让 Claude Code 每次启动都自动读取一些预设信息？\n这就是 CLAUDE.md 的作用。你可以在里面写明项目的技术栈、代码风格偏好、注意事项等等。Claude Code 每次启动都会自动加载这个文件。\n用 /init 命令可以自动生成一份初始的 CLAUDE.md，然后根据需要修改。比如我在末尾加了一条：「每次回答的最后必须追加 Happy Coding」。重启后随便问个问题，Claude Code 果然在回复末尾加上了这句话。\nCLAUDE.md 分两个级别：\n项目级：放在项目根目录，对这个项目生效，可以提交到 Git 供团队共享 用户级：放在用户目录下，对所有项目生效 用 /memory 命令可以快速打开对应级别的 CLAUDE.md 文件，不用在文件管理器里翻。\nHook：自动化你的工作流 # Hook 允许你在特定时机（工具执行前/后、执行失败时等）自动运行一段自定义逻辑。一个典型的应用场景是代码格式化：让 Claude Code 每次写完文件后自动跑一遍 Prettier。\n配置方式：\n执行 /hooks 进入 Hook 配置页面 选择触发时机，比如 Post Tool Use（工具执行后） 选择匹配的工具，比如 Write 或 Edit 输入要执行的命令 命令大概长这样：\necho \u0026#39;$TOOL_INPUT\u0026#39; | jq -r \u0026#39;.file_path\u0026#39; | xargs prettier --write Claude Code 在触发 Hook 时会传入一个 JSON，其中 file_path 就是刚编辑完的文件路径。用 jq 提取路径，再传给 Prettier 格式化。\nHook 的保存位置有三个选项：\n本地项目级（settings.local.json）：只在本机本项目生效，不进 Git 项目级（settings.json）：所有使用这个项目的人都生效，会随 Git 分发 用户级：对当前用户的所有项目生效 Agent Skill：可复用的提示词模板 # 如果你经常需要 Claude Code 按特定格式输出内容（比如每天写一份包含日期、开发摘要、开发详情的开发日志），每次手动粘贴格式要求太麻烦。这种场景适合用 Agent Skill 来解决3。\nAgent Skill 本质上是一个动态加载的 Prompt。你把格式要求写在 skill.md 里，Claude Code 会根据用户请求自动匹配并加载对应 Skill。它也可以通过 /skill-name 的方式手动调用，省去模型意图识别的环节。\nSubAgent：独立上下文的利器 # Agent Skill 运行时完全共享当前对话的上下文。这意味着 Skill 执行过程中的所有日志、思考过程都会塞进你的上下文窗口。对于轻量任务没什么问题，但如果让 Skill 去审核一个几万行代码的项目，中间过程会把上下文撑爆，Token 消耗飙升，模型也会因为上下文过载而变慢。\nSubAgent 解决了这个问题。它拥有完全独立的上下文，启动时开辟一个全新的对话窗口，所有中间过程都在那个窗口里完成。只有最终的执行结果会汇报回主对话。\n两者在上下文处理上的区别决定了各自的适用场景：\n维度 Agent Skill SubAgent 上下文 共享主对话 完全独立 适合场景 与上下文强关联、对上下文影响小 与上下文弱关联、对上下文影响大 典型任务 写开发总结、格式转换 代码审核、大规模重构分析 创建 SubAgent 的流程：执行 /agent 命令，选择「Create New Agent」，描述它的职责，配置可用工具（比如只读权限）和使用的模型。Claude Code 会生成一份初始描述文件，你可以根据需要修改。之后在对话中提出相关需求，Claude Code 会自动调用对应的 SubAgent。\nPlugin：一键安装全家桶 # Plugin 把 Skill、SubAgent、Hook、MCP 等能力打包成一个安装包，一键就能给 Claude Code 装备一整套高级能力。\n在 Claude Code 里执行 /plugin 进入插件管理器，可以浏览、安装和查看已安装的插件。安装时选择生效范围（用户级、项目级等），然后重启 Claude Code 即可生效。\n举个例子，Anthropic 官方提供了一个 frontend-design 插件，内置了一套 UI 设计规范。安装后让 Claude Code 做前端页面，它会自动加载这套规范，输出的界面在配色、排版、交互上都比默认效果好看不少。\nPlugin 市场正在快速增长。除了 UI 设计类的插件，还有针对特定编程语言的 LSP 插件等。如果你觉得自己积累的配置足够成熟，也可以打包成 Plugin 分享给团队或社区。\n总结 # 这篇文章覆盖了 Claude Code 从入门到进阶的完整链路：\n三种模式（默认/自动/规划）灵活切换，适应不同场景 Plan Mode 先规划后执行，降低改动风险 后台任务 解决长时间运行命令的阻塞问题 回滚功能 提供安全网，但复杂场景还是得靠 Git 图片和 Figma MCP 实现设计稿到代码的转换 上下文管理（/compact、/clear）控制 Token 消耗 CLAUDE.md 让 Claude Code 自动读取项目配置 Hook 在关键节点自动执行自定义逻辑 Agent Skill 和 SubAgent 分别处理轻量和重量级任务 Plugin 一键安装整套能力扩展 每个功能单独看都不复杂，组合起来就是一个相当完备的开发工作流。希望这篇梳理对你有帮助。\n具体配置方法网上有很多教程，核心就是设置几个环境变量来指定模型接口地址和认证信息。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n关于 MCP 的详细使用方法和设计原理，我在之前的文章里有专门讨论。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n关于 Agent Skill 的完整用法和底层设计思路，可以参考我之前写的 Agent Skill 专题文章。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2026-01-07","externalUrl":null,"permalink":"/posts/2026/01/claude-code-guide/","section":"所有文章","summary":"","title":"Claude Code 全面上手指南：不只是写代码","type":"posts"},{"content":"","date":"2026-01-07","externalUrl":null,"permalink":"/tags/skill/","section":"标签","summary":"","title":"Skill","type":"tags"},{"content":"","date":"2026-01-07","externalUrl":null,"permalink":"/en/categories/tutorials/","section":"Categories","summary":"","title":"Tutorials","type":"categories"},{"content":"","date":"2026-01-07","externalUrl":null,"permalink":"/categories/%E6%8A%80%E6%9C%AF%E6%95%99%E7%A8%8B/","section":"分类","summary":"","title":"技术教程","type":"categories"},{"content":" 前言 # 最近在用 Claude Code 的时候，我发现一个功能越用越顺手，就是 Agent Skill。一开始我只把它当成一个「提示词存档」，后来才发现它的设计比我想象的要精巧得多。这篇文章聊聊 Agent Skill 到底是什么、怎么用，以及底层的设计思路，顺便和 MCP 做个对比，帮你在实际场景中选对工具。\nAgent Skill 是什么 # 最简单的理解：Agent Skill 就是一份大模型可以随时翻阅的说明文档。\n举个例子，你想做一个智能客服，可以在 Skill 里面写清楚「遇到投诉先安抚情绪，不得随意承诺」；想做会议总结，可以规定「必须按参会人员、议题、决定这个格式输出」。这样一来不用每次对话都重复粘贴一长串要求，模型自己翻翻这份文档就知道该怎么干活了1。\n当然，「说明文档」只是一个方便入门的理解。Skill 能做的事情远不止于此，后面会慢慢展开。\n基础用法：创建一个会议总结助手 # 以 Claude Code 为例，使用 Skill 的第一步是创建它。\nClaude Code 要求把 Skill 放在用户目录下的 .claude/skills/ 文件夹中。我们创建一个文件夹叫「会议总结助手」，文件夹名称就是 Skill 的名称。然后在里面新建一个 skill.md 文件。\n这个文件分两部分：\n头部是元数据（metadata），用三条横线包裹，包含 name 和 description 两个字段。name 必须和文件夹名字一致，description 则是向模型说明这个 Skill 是干什么的。\n剩余部分是指令（instruction），用来详细描述模型需要遵循的规则。比如我规定了必须总结参会人员、议题和决定这三个方面，并且给了一个输入输出的示例，确保模型真的理解了要求。\n创建完成之后，打开 Claude Code，问它「你有哪些 Skill」，它就会列出刚才创建的那个。然后输入一段会议录音文本，让它总结。Claude Code 会先识别到这个请求跟「会议总结助手」相关，请求使用这个 Skill，得到你的同意后读取 skill.md 的内容，最后按照规定格式输出总结。\n整个过程非常直观。\n流程拆解：Skill 的运行机制 # 基础用法看完了，不妨想想刚才到底发生了什么。\n整个流程中有三个角色：用户、Claude Code（宿主程序）、以及背后的大模型。流程是这样的：\n用户输入请求 Claude Code 把用户请求连同所有 Skill 的名称和描述一起发给大模型 大模型发现用户请求跟「会议总结助手」匹配，把这个信息返回给 Claude Code Claude Code 去读取匹配到的 Skill 的 skill.md 全文 Claude Code 把用户请求和完整的 skill.md 内容发给大模型 大模型按照 Skill 要求生成响应 这里有一个关键细节：第 2 步只发了名称和描述，第 4 步才读取全文。也就是说，哪怕你装了十几个 Skill，模型一开始看的也只是一份轻量级的目录。这引出了 Skill 的第一个核心机制：按需加载。\n高级用法一：Reference（条件加载文件） # 按需加载已经挺省 Token 了，但还不够极致。\n假设你的会议总结助手越来越高级：当会议涉及花钱时，能在总结里标注是否符合财务合规；涉及合同时，能提示法务风险。要实现这些，Skill 就需要知道相关的财务规定和法律条文。如果把所有这些内容都写进 skill.md，文件会变得非常臃肿，哪怕只是开个简单的技术复盘会，也要被迫加载一堆用不上的财务条款。\n能不能做到更细粒度的按需加载？比如只有当会议内容真的聊到了钱，才把财务规定加载进来？\n这就是 Reference 解决的问题。\n我们创建一个 集团财务手册.md 文件，写明各种费用的报销标准，然后在 skill.md 里加一条规则：仅在提到钱、预算、采购、费用的时候触发，触发时读取这个文件并根据内容判断金额是否超标。\n实际测试一下：如果会议内容涉及了预算，Claude Code 会先读取 skill.md，发现跟钱相关，再去加载财务手册，最终在总结中给出财务提醒。如果是个跟钱无关的技术复盘会，那个财务文件就只会安静地躺在硬盘上，不会占用哪怕一个 Token。\nReference 的核心特性：条件触发，只在需要的时候才加载，不需要就完全不碰。\n高级用法二：Script（执行代码） # 查资料只是第一步，能直接跑代码把活干了，才是真正的自动化。这就用到了 Skill 的另一大能力：Script。\n我们在文件夹里创建一个 upload.py 脚本用于上传文件，然后在 skill.md 里加一条规则：如果用户提到了「上传」「同步」「发送到服务器」，就必须运行这个脚本。\n实际测试时，Claude Code 会正常生成会议总结，然后直接执行 upload.py 完成上传。这里有个值得注意的细节：Claude Code 申请执行脚本时，并没有去读取脚本内容。它只关心怎么跑和跑出来的结果，至于代码写了什么，它毫不在意。\n这意味着哪怕你的脚本写了 1 万行复杂的业务逻辑，它消耗的模型上下文也几乎是零。\n所以 Reference 和 Script 虽然都属于高级功能，但对模型上下文的影响截然不同：\nReference 是读：把文件内容加载到上下文中，会消耗 Token Script 是跑：只执行不读取，几乎不占用上下文 渐进式披露：Skill 的三层架构 # 把上面的内容串起来，Skill 的设计其实是一个精密的渐进式披露结构，一共三层：\n第一层：元数据层。 所有 Skill 的名称和描述，始终加载，相当于目录。模型每次回答前都会扫一遍，判断用户的问题是否跟某个 Skill 匹配。\n第二层：指令层。 对应 skill.md 中除了元数据以外的部分。只有当模型发现用户问题跟某个 Skill 匹配时才加载，所以叫按需加载。\n第三层：资源层。 包含 Reference 和 Script（官方规范中还有 Assets，但与 Reference 有重叠，暂不展开）。这一层只在模型根据指令层判断出需要具体资源时才会触发，是在按需加载的基础上又做了一次按需加载，可以叫「按需中的按需」。\n三层之间层层递进，每一层的加载都建立在上层的判断之上，把 Token 消耗压到了最低。\nSkill 和 Prompt Engineering 是什么关系 # 聊到这里，另一个经常被问到的问题自然就浮现出来了：Skill 和 Prompt Engineering 到底什么关系？感觉都在「教模型做事」，有啥区别？\n我的理解是，它们解决的是不同层级的问题。\nPrompt Engineering 解决的是「如何思考」。 它的核心工作是引导模型进行正确的理解和推理：明确角色、提供上下文、规范输出格式、减少幻觉。本质上它属于认知层，决定模型「要做什么」「怎么拆解问题」「是否需要外部能力」。但 Prompt 本身不负责执行任何实际操作。\nSkill 解决的是「如何行动」。 它把模型的决策转化为可执行的行为：调用函数、跑脚本、读写文件。Skill 不参与思考，只负责拿到指令后干活并返回结果2。\n打个不太严谨但好记的比方：Prompt Engineering 像是给新人写了一份入职手册，告诉他遇到什么情况该怎么判断；Skill 则是给他配了一套工具箱，判断完了直接上手操作。一个是脑，一个是手。\n理解了这层关系，再看前面的三层架构就很清晰了：Skill 的指令层承载的是 Prompt Engineering 的成果，资源层承载的才是真正的执行能力。\nAgent Skill 和 MCP 怎么选 # 聊完用法，很多人会有个感觉：Skill 和 MCP 是不是有点像？本质上都是让模型连接和操作外部世界。\nAnthropic 官方有一句话把两者的关系说得很到位：\nMCP connects Claude to data. Skills teach Claude what to do with that data.\nMCP 给大模型供给数据，比如查询销售记录、读取物流状态；Skill 教会大模型如何处理数据，比如会议总结必须包含议题、汇报文档必须附上具体数据。\n有人可能会问：Skill 里面也能写代码连接数据，干嘛不直接用 Skill 把两件事都干了？\n确实能干，但不代表适合干。就像瑞士军刀也能切菜，但没人真拿它做饭。MCP 本质上是一个独立运行的服务程序，Skill 本质上是一段说明文档，两者在安全性、稳定性和适用场景上差别很大。Skill 更适合跑轻量脚本和处理简单逻辑，在复杂的数据连接方面不如 MCP 可靠3。\n实际场景中，很多时候需要把两者结合起来用：MCP 负责连接数据源，Skill 负责定义处理规则，各司其职。\n严格来说，Skill 不只是静态的说明文档。它还支持条件加载外部文件和执行代码脚本，具备一定的动态能力。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n这里的「不参与思考」是相对于 Prompt Engineering 而言的。Skill 的指令层本身包含 Prompt Engineering 的成分，但资源层的 Reference 和 Script 纯粹是执行，不涉及推理。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nSkill 中的脚本由 Claude Code 直接执行，缺乏 MCP 那样的沙箱隔离和权限管控机制，不适合处理敏感或高风险的数据操作。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-12-21","externalUrl":null,"permalink":"/posts/2025/12/agent-skill-architecture/","section":"所有文章","summary":"","title":"Agent Skill 的三层架构与设计哲学","type":"posts"},{"content":"","date":"2025-01-12","externalUrl":null,"permalink":"/tags/prd/","section":"标签","summary":"","title":"PRD","type":"tags"},{"content":"","date":"2025-01-12","externalUrl":null,"permalink":"/en/tags/product-manager/","section":"Tags","summary":"","title":"Product Manager","type":"tags"},{"content":"","date":"2025-01-12","externalUrl":null,"permalink":"/en/categories/technical-thinking/","section":"Categories","summary":"","title":"Technical Thinking","type":"categories"},{"content":"","date":"2025-01-12","externalUrl":null,"permalink":"/tags/%E4%BA%A7%E5%93%81%E7%BB%8F%E7%90%86/","section":"标签","summary":"","title":"产品经理","type":"tags"},{"content":" 前言 # 最近在负责一个 Agent 产品，PRD 写到一半的时候我意识到一件事：以前那套写法，用在 Agent 上有点像拿螺丝刀拧钉子，不是完全不行，但就是使不上劲。功能列表、页面结构、交互流程这些老朋友依然在列，但 Agent 真正需要定义的东西，它们覆盖不到。\n于是我把整份 PRD 推倒重写了一遍，把思路从「描述功能」切换到「描述决策」。这篇文章聊聊我为什么要换写法，以及换完之后的结构是什么样的。\n传统 PRD 的盲区 # 传统 PRD 的底层假设是系统行为可预期，你定义好每个状态的流转路径就行了。这套方法在确定性的产品里很好使，但 Agent 产品天生带着不确定性：同样的用户输入，在不同上下文下可能对应完全不同的处理路径。\n传统 PRD 在面对 Agent 时有几个明显的盲区：\n意图理解没有定义。 传统产品里用户通过按钮、菜单、表单来表达意图，意图是显式的。Agent 产品里用户说的是自然语言，意图是隐式的1。「帮我查一下上个月的数据」可能是想看总览，可能是想对比环比，也可能是要导出给老板。如果不把意图空间先拆清楚，后面的设计就是空中楼阁。\n工具调用的判断条件缺失。 很多 Agent PRD 会列一串工具能力：「支持知识库检索、支持搜索、支持调用工单系统。」但真正决定体验的是这些工具在什么条件下该调、按什么顺序调、调失败了怎么办2。工具调用本身就是一个产品决策，不能丢给研发自己去判断。\n边界条件被当成异常处理。 传统产品里异常处理是兜底，放在文档最后补一段就行。但 Agent 产品里，信息不足、工具失败、数据冲突、幻觉风险这些情况不是小概率事件，而是每天都在发生的主流程的一部分3。把它们放到「异常处理」的框架下去写，本身就是一种误判。\n换个思路：从决策流开始写 # 意识到这些问题之后，我把 PRD 的起点从「功能模块」换成了「决策流」。\n核心思路是这样的：用户说了一句话之后，系统先判断意图是否清晰，不清晰就追问；清晰之后判断需不需要调工具；调完工具之后判断是不是高风险动作，是的话必须让用户确认。整条链路就是一系列判断节点。\n我用 Mermaid4 画了一下这个逻辑：\ngraph TD A([用户输入]) --\u003e B{是否识别清晰意图} B -- 否 --\u003e C[追问补充信息] B -- 是 --\u003e D{是否需要外部工具} D -- 否 --\u003e E[直接生成结果] D -- 是 --\u003e F[调用知识库/搜索/业务系统] F --\u003e G{是否高风险动作} G -- 是 --\u003e H[请求用户确认] G -- 否 --\u003e I[执行并返回结果] 这不是一个放之四海皆准的模板，不同业务场景肯定要调整分支和判断条件。但它给了 PRD 一个骨架：每个菱形节点都是一个需要明确定义的判断规则，每个矩形节点都是一个需要写清楚的行为描述。\n我怎么写意图拆解 # 意图拆解我一般用结构表来写，不是简单列几个意图名称就完事。每个意图至少要覆盖这些字段：意图名称、典型表达、真实目标、优先级、是否允许自动执行、是否需要二次确认。\n举个实际例子。同样是「我无法登录」，背后至少有四种意图：忘记密码、账号被锁、新员工未开通、SSO 配置异常。这四种意图对应的处理路径完全不同。如果 PRD 只写一句「系统自动识别问题类别并创建工单」，研发在实现的时候只能靠猜。\n拆完意图之后还有一件事很重要：信息不足时怎么办。用户可能一句话说了两个诉求，也可能关键信息完全没给。这时候是追问一轮直接给答案，还是追问到底？追问两轮还是不够信息，是降级回答还是报错？这些规则要提前定死，不能留到线上再调。\n我怎么写工具调用 # 工具调用我不会写成能力清单，而是写成条件判断：\n用户的问题是否涉及内部数据？涉及的话先查知识库，不涉及的话直接回答 知识库返回的结果是否足够回答用户的问题？不够的话再调搜索补全 是否涉及写操作（创建、修改、删除）？涉及的话必须经过用户确认 工具调用超时或返回异常，系统是降级回答、报错、还是保存草稿等用户重试 多个工具返回的结果互相矛盾，优先信哪个数据源 这里面每一个条件都是一个产品决策。写得越明确，研发实现的时候偏离就越小，上线后扯皮也越少。\n每多调一个工具，就多一层延迟、多一个失败点。所以工具不是越多越好，能不调就不调。一句话能答明白的事情，非要绕三个工具再总结一遍，体验只会更差。\n我怎么写边界条件 # 边界条件我现在的做法是和主流程同等对待，不是放在文档最后的补充章节，而是嵌入到每个判断节点里。\n具体来说，我会关注几类：\n信息不足：追问策略是什么，追问几轮后怎么收束 工具失败：每个工具挂了之后的降级方案 高风险动作：哪些操作必须确认、哪些可以自动执行、哪些永远不能自动执行 数据冲突：多个信息源结果矛盾时信谁的，还是展示给用户自己判断 幻觉控制：模型在没有充分依据时能不能生成内容，不能的话怎么告知用户 这些规则看起来枯燥，但它们才是 Agent 产品「可控」和「失控」的分界线。\n完整的 PRD 结构 # 踩完一轮之后，我现在的 Agent PRD 大致按这个结构来写：\n场景定义：为什么这个场景需要 Agent 而不是传统功能？如果一件事规则极稳定、输入极结构化、不需要多轮判断，其实不一定要上 Agent3。这一段可以过滤掉不少伪需求。\n意图拆解：结构表，把每个场景下可能的意图、触发条件、优先级、处理方式列出来。\n决策路径：就是上面那张 Mermaid 图展开的内容，每个判断节点要写清楚判断条件和分支走向。\n工具调用规则：触发条件、禁止条件、调用顺序、失败降级。\n边界条件：和主流程同等重要，嵌入到每个节点而不是单独成章。\n结果定义：什么叫完成、什么叫部分完成、什么叫失败但有可用中间结果。\n评估指标：意图识别准确率、工具调用成功率、任务完成率、人工接管率、结果采纳率，不能只看 DAU。\n写在最后 # 回头看，传统 PRD 和 Agent PRD 最根本的区别在于：传统 PRD 描述的是系统在不同状态下的表现，Agent PRD 描述的是系统在不同判断下的行为。一个是静态的页面和流程，一个是动态的意图和决策。\n想通这件事之后，写法自然就变了。如果你也在做 Agent 产品，不妨拿自己的 PRD 检查一下：意图拆清楚了吗？工具调用的判断条件写了吗？边界条件覆盖全了吗？如果有一个答不上来，问题大概不在文档，而在产品设计本身还没想透。\nhttps://www.promptingguide.ai/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://platform.claude.com/docs/en/agents-and-tools/tool-use/overview\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.anthropic.com/engineering/building-effective-agents\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://mermaid.js.org\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-01-12","externalUrl":null,"permalink":"/posts/2025/01/agent-prd-writing/","section":"所有文章","summary":"","title":"写 Agent PRD 踩过的坑，以及我现在的写法","type":"posts"},{"content":"","date":"2024-12-15","externalUrl":null,"permalink":"/tags/ltsc/","section":"标签","summary":"","title":"LTSC","type":"tags"},{"content":"","date":"2024-12-15","externalUrl":null,"permalink":"/en/categories/tinkering/","section":"Categories","summary":"","title":"Tinkering","type":"categories"},{"content":"","date":"2024-12-15","externalUrl":null,"permalink":"/tags/windows/","section":"标签","summary":"","title":"Windows","type":"tags"},{"content":" 前言 # 我的日常主力是 Mac，但手上还有一台 Linux 笔记本，跑了双系统：一边是 NixOS，另一边装了个 Windows 11 IoT LTSC。这台机器上的 Windows 用途比较轻量，主要处理一些 macOS 和 Linux 上不方便做的事情。\n为什么选 LTSC？我就是想要一个干净的 Windows。没有 Microsoft Store、没有 Copilot、没有一堆预装 UWP 应用，系统占用也比普通版低不少。如果你也喜欢从零开始定制系统，LTSC 是个不错的选择。\n这篇文章记录一下我的安装和配置过程，仅供参考。\n安装前要知道的事 # LTSC 和 IoT LTSC 怎么选 # LTSC（Long-Term Servicing Channel）1 是 Windows 企业版的特殊分支，面向对稳定性和连续性有极高要求的设备。普通 LTSC 提供 5 年支持，IoT LTSC 提供 10 年。功能上两者几乎一致，主要区别在于：\nIoT LTSC 支持数字权利许可证激活 IoT LTSC 默认不开启 BitLocker 加密 IoT LTSC 支持不满足 TPM 要求的设备安装 不过有一点需要注意：IoT LTSC 默认只有英文，安装完需要手动下载中文语言包。如果你嫌麻烦，普通 LTSC 自带多语言，装完直接是中文，省一步。\n备份数据 # 重装前备份好所有数据、软件配置和个人文件，这是老生常谈了。因为是双系统，我还要确保 NixOS 的分区和引导配置不被破坏，建议先记清楚分区布局。\n准备基础程序 # 提前下载好以下东西，避免装完系统之后网卡驱动都没有、连不上网的尴尬：\n以太网和 Wi-Fi 网卡驱动 系统激活工具（HEU2 即可） 下载和安装 # 下载镜像 # Windows 11 IoT LTSC 的官方下载渠道需要填写企业信息3，流程比较繁琐。更方便的方式是通过 MAS 提供的直链下载4，免注册，直接拿到 ISO。\n制作启动盘 # 我习惯用 Ventoy5，把 ISO 直接扔进 U 盘就行，省得每次用 Rufus 写入。\n安装过程 # 进 PE 格式化目标分区，然后直接启动 Windows 镜像安装。安装过程没什么特别的，跟着向导走就行。双系统用户注意选对分区，别覆盖了 Linux 那边的数据。装完后如果 GRUB 引导被覆盖，用 NixOS 的安装 U 盘修复一下就行。\n系统配置 # 装完系统之后，才是真正折腾的开始。\n添加中文语言包 # IoT LTSC 默认只有英文，安装完进入系统设置，连接网络后下载中文语言包，把显示语言和区域格式都改过来。如果觉得这步麻烦，可以考虑装普通 LTSC，自带中文。\n禁用 Windows 保留存储 # Windows 默认会预留一部分磁盘空间给系统更新，如果不需要可以关掉：\nDISM.exe /Online /Set-ReservedStorageState /State:Disabled 删除休眠文件 # 如果不使用休眠功能，休眠文件会白白占用几个 GB 的磁盘空间。在管理员终端中执行：\npowercfg -h off 禁用 Windows Defender # Windows Defender 本身是不错的杀毒软件，但误报率太高，而且经常吃资源让风扇起飞。我用的是联想知识库提供的工具来禁用，Windows 10/11 通杀。不过禁用之后建议装一个靠谱的第三方杀毒软件，裸奔还是不太安全。\n卸载 Edge 浏览器 # Edge 本身是个还行的 Chromium 浏览器，但它实在太烦了：访问 ChatGPT 弹 Copilot 横幅、下载 Chrome 推 Edge 横幅、小组件还时不时冒出来。用 Remove-MS-Edge6 可以卸载，注意保留 WebView2，只选第一个选项卸载浏览器本体就行。\n更新 WebView2 # 很多第三方应用依赖 WebView27，卸载 Edge 之后记得更新到最新稳定版，从微软开发者网站下载离线安装包。\n安装 VC++ 运行库 # 大部分 Windows 应用都需要 VC++ 运行库，从微软官网下载 VC++ 2015-2022 合集安装。\n安装 Microsoft Store（可选） # 如果你需要 UWP 应用，用管理员权限打开 PowerShell 执行：\nwsreset -i 没有进度条，装完会有通知弹出来。\nWindows Update # 连接网络后先跑一次完整的系统更新。下载慢的话可以临时开启传递优化（P2P 加速），更新完记得关掉。\n禁用硬件驱动自动更新 # 建议在完成一次完整更新之后禁用驱动自动更新，这样显卡之类的驱动就不会被 Windows Update 覆盖，可以自己去官网下载 WHQL 认证版本安装。\n写在最后 # 整体折腾下来，IoT LTSC 给我的感觉就是干净和稳定。没有乱七八糟的预装应用，没有 Copilot 强推，IoT 版默认连 AI 相关组件都不带，系统资源占用也比普通版低。如果需要打游戏，安装对应的运行库和组件就行，LTSC 本身不会有什么限制。\n对我来说，从一个干净的系统开始，按自己的需求一步步装软件、做配置，这个过程本身就是折腾的乐趣所在。\n参考文献 # https://massgrave.dev/windows_ltsc_links https://info.microsoft.com/ww-landing-windows-11-enterprise.html https://learn.microsoft.com/en-us/windows/whats-new/ltsc/overview\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/zbezj/HEU_KMS_Activator\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://info.microsoft.com/ww-landing-windows-11-enterprise.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://massgrave.dev/windows_ltsc_links\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.ventoy.net\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/zoicware/Remove-MS-Edge\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://developer.microsoft.com/en-us/microsoft-edge/webview2/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-12-15","externalUrl":null,"permalink":"/posts/2024/12/windows-11-iot-ltsc-guide/","section":"所有文章","summary":"","title":"Windows 11 IoT LTSC 折腾记录","type":"posts"},{"content":"","date":"2024-12-15","externalUrl":null,"permalink":"/categories/%E6%8A%98%E8%85%BE%E8%AE%B0%E5%BD%95/","section":"分类","summary":"","title":"折腾记录","type":"categories"},{"content":"","date":"2024-11-06","externalUrl":null,"permalink":"/tags/macos/","section":"标签","summary":"","title":"MacOS","type":"tags"},{"content":"","date":"2024-11-06","externalUrl":null,"permalink":"/tags/mlx/","section":"标签","summary":"","title":"MLX","type":"tags"},{"content":"","date":"2024-11-06","externalUrl":null,"permalink":"/tags/python/","section":"标签","summary":"","title":"Python","type":"tags"},{"content":"","date":"2024-11-06","externalUrl":null,"permalink":"/tags/whisper/","section":"标签","summary":"","title":"Whisper","type":"tags"},{"content":" 前言 # 最近有音频转录的需求，正好刷到 Awni Hannun 发的一条推文，最新的 MLX Whisper 速度更快了，M2 Ultra 上 12.3 秒就能转录 12 分钟的音频，接近 60 倍实时速度：\nThe latest MLX Whisper is even faster.\nWhisper v3 Turbo on an M2 Ultra transcribes ~12 minutes in 12.3 seconds. Nearly 60x real-time.\npip install -U mlx-whisper pic.twitter.com/DcKE0TRcbv\n\u0026mdash; Awni Hannun (@awnihannun) November 1, 2024 刚好手上有 Mac，就想试试这个方案。研究了一下发现用起来非常简单，记录一下过程。\n安装 # MLX Whisper 是基于 Apple MLX 框架的 Whisper 实现，跑在 Apple Silicon 上效率很高。安装方式有两种。\n常规方式 # pip install -U mlx-whisper 用 uv 安装 # 我个人有点系统洁癖，不太喜欢往全局环境里装东西。uv 1 是一个 Python 包管理工具，它的 uv tool install 可以把命令行工具安装到隔离环境中，不会污染系统 Python。如果你也有类似习惯，推荐这种方式：\nuv tool install mlx-whisper 安装完之后 mlx_whisper 命令就可以直接用了。\n使用 # 命令行直接用 # mlx_whisper audio.mp3 --model mlx-community/whisper-large-v3-turbo 写个脚本批量处理 # 我写了一个小脚本，支持一次传入多个音频文件，转录完会自动保存为同名的 .txt 文件：\nimport sys import os import mlx_whisper if len(sys.argv) \u0026lt; 2: print(\u0026#34;用法: python whisper.py \u0026lt;音频文件\u0026gt; [音频文件...]\u0026#34;) sys.exit(1) model = \u0026#34;mlx-community/whisper-large-v3-turbo\u0026#34; for file in sys.argv[1:]: print(f\u0026#34;正在转录: {file}\u0026#34;) result = mlx_whisper.transcribe(file, path_or_hf_repo=model) output = \u0026#34;\\n\\n\u0026#34;.join(seg[\u0026#34;text\u0026#34;].strip() for seg in result[\u0026#34;segments\u0026#34;]) txt_path = os.path.splitext(file)[0] + \u0026#34;.txt\u0026#34; with open(txt_path, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: f.write(output + \u0026#34;\\n\u0026#34;) print(f\u0026#34;已保存: {txt_path}\u0026#34;) print() 用法很简单，把音频文件拖进去就行。因为我是用 uv 管理的，脚本也直接用 uv 运行，--with 会自动临时安装依赖：\nuv run --with mlx-whisper python whisper.py 音频1.mp3 音频2.m4a 模型存储位置 # 模型会通过 Hugging Face Hub 自动下载，默认缓存在：\n~/.cache/huggingface/hub/models--mlx-community--whisper-large-v3-turbo/ 如果以后不想用了，直接删掉这个目录就行，不留残余。\n小结 # MLX Whisper 配合 Apple Silicon 确实很快，基本就是丢个文件进去等几秒出结果。安装和清理也都很干净，推荐有 Mac 的朋友试试。\nuv，Astral 出品的 Python 包管理工具\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-11-06","externalUrl":null,"permalink":"/posts/2024/11/mlx-whisper-transcription/","section":"所有文章","summary":"","title":"用 MLX Whisper 在 Mac 上极速转录音频","type":"posts"},{"content":"","date":"2024-08-10","externalUrl":null,"permalink":"/tags/networking/","section":"标签","summary":"","title":"Networking","type":"tags"},{"content":" 前言 # 在 2022 年 1 月份，我在小黄鱼上购入了一台搭载 J4125 处理器的 2.5G 软路由，从此开启了我的折腾之旅。2 月份猫棒开始流行，原本打算也跟风购买一个来替代家里的光猫，后来听说发热量比较大，温度过高后就会断流，于是就打消了念头。\n2024 年的下半年，中兴的 F7015tv3/F7005tv3 突然火了起来，有着很低的功耗1，带有一个 2.5G 网口、一个 IPTV 口、两个千兆网口。两款光猫的区别在于 F7015tv3 多了一个电话口。由于家里宽带送了电话（虽然一年也接不到几个电话），最后在小黄鱼上购买了 F7015tv3，到手后发现是真的小巧，跟我的 iPhone 13 mini 一样长。\n硬件信息 # # cat /proc/capability/boardinfo system:LINUX cpufac:ZXIC cpumod:ZX279133 2gwlmod:MTK 5gwlmod:MTK cpufre:1000MHZ cpunum:2 flshcap:256MB ddrcap:512MB 准备工作 # 打开老光猫的后台，使用超级管理员账户登录。这里提供北京联通的登录方法：打开 192.168.1.1，按下 F12 键，在 Console 里输入以下内容。\ndocument.getElementById(\u0026#34;loginfrm\u0026#34;).setAttribute(\u0026#34;method\u0026#34;, \u0026#34;get\u0026#34;); document.getElementById(\u0026#34;username\u0026#34;).value = \u0026#34;CUAdmin\u0026#34;; document.getElementById(\u0026#34;password\u0026#34;).value = \u0026#34;CUAdmin\u0026#34;; document.getElementById(\u0026#34;loginfrm\u0026#34;).submit(); submitFrm(); 记录一下 SN/MAC 地址、设备号以及连接配置 VLAN 等信息（截图或拍照保存下来）。\n配置新光猫 2 # 使用光猫的超级账户 useradmin，密码 nE7jA%5m 登录后台，点击管理 → 上行方式，切换 XGPON 或者 XEPON，这里按照自己的宽带类型操作。\n开启光猫的 Telnet3，电脑网卡的 MAC 地址要修改为 000729553357，然后执行下面的命令，提示 reboot 重启后等待光猫自动重启即可。\nzteOnu --telnet 删除原有的网络配置： sendcmd 1 DB p WANC sendcmd 1 DB delr WANC 0 sendcmd 1 DB delr WANC 1 有几个删几个，修改 WANC 后的数字即可。\n新建网络连接，这里按照旧光猫上的配置操作即可。\n使用 Telnet 修改光猫参数。setmac show2 查看系统参数。\n切换区域 # # cat /etc/init.d/regioncode 200:Jiangsu 201:Xinjiang 202:Hainan 203:Tianjin 204:Anhui 205:Shanghai 206:Chongqing 207:Beijing 208:Sichuan 209:Shandong 210:Guangdong 211:Hubei 212:Fujian 214:Zhejiang 215:Shanxi 216:Hunan 217:Yunnan 218:Xizang 219:Heilongjiang 220:Guizhou 221:Shanxi2 222:Hebei 223:Ningxia 224:Guangxi 225:Jiangxi 226:Gansu 227:Qinghai 229:Liaoning 230:Jilin 231:Neimeng 232:Henan 234:TelecomInstitute # 切换北京区域 upgradetest sdefconf 207 修改 MAC 地址 # setmac 1 32769 12:34:56:78:90:12 修改设备码前 6 位 # setmac 1 768 xxxxxx 修改设备码后 17 位数字 # setmac 1 512 xxxxxxxxxx ITMS 欺骗 # sendcmd 1 DB set PDTCTUSERINFO 0 Status 0 sendcmd 1 DB set PDTCTUSERINFO 0 Result 1 sendcmd 1 DB save 修改完毕后，连接光纤，重启即可。\nhttps://www.acwifi.net/28124.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.chiphell.com/thread-2607258-1-1.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/Septrum101/zteOnu\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-08-10","externalUrl":null,"permalink":"/posts/2024/08/beijing-unicom-replaced-2.5g-optical-modem/","section":"所有文章","summary":"","title":"记一次北京联通更换 2.5G 光猫","type":"posts"},{"content":"","date":"2024-07-17","externalUrl":null,"permalink":"/tags/android/","section":"标签","summary":"","title":"Android","type":"tags"},{"content":"","date":"2024-07-17","externalUrl":null,"permalink":"/tags/arch/","section":"标签","summary":"","title":"Arch","type":"tags"},{"content":"","date":"2024-07-17","externalUrl":null,"permalink":"/tags/cloud/","section":"标签","summary":"","title":"Cloud","type":"tags"},{"content":"","date":"2024-07-17","externalUrl":null,"permalink":"/tags/docker/","section":"标签","summary":"","title":"Docker","type":"tags"},{"content":"","date":"2024-07-17","externalUrl":null,"permalink":"/tags/linux/","section":"标签","summary":"","title":"Linux","type":"tags"},{"content":"","date":"2024-07-17","externalUrl":null,"permalink":"/tags/nixos/","section":"标签","summary":"","title":"NixOS","type":"tags"},{"content":"","date":"2024-07-17","externalUrl":null,"permalink":"/tags/ubuntu/","section":"标签","summary":"","title":"Ubuntu","type":"tags"},{"content":" 前言 # 当前市面上已经有很多云手机厂商，虽然比较稳定，但把数据全都放在别人手里感觉不是很安全。谁知道每次进行操作时厂商会不会进行录屏等行为呢。\nGithub1 上有一个开源项目 Redroid2，可以使用 Docker 运行安卓容器。我已经使用了很长时间，运行很稳定。\n准备环境 # 需要一台服务器，AMD64 和 ARM64 架构都可以。\n支持的系统如下，点击即可查看各个发行版的部署操作：\nAlibaba-Cloud-Linux Amazon-Linux Arch-Linux CentOS Debian Deepin Fedora Gentoo Kubernetes LXC Mint OpenEuler PopOS Ubuntu WSL 如果你是新手，推荐使用 Ubuntu、Arch Linux 或者 NixOS。其中 Ubuntu 需要加载内核模块，Arch Linux 和 NixOS 直接更换 zen 内核即可。\n操作部分 # 这里只介绍上面所说的三个操作系统，其他的系统可自行阅读官方文档。\nUbuntu # 建议不要使用最新的 Ubuntu，可能会有各种小问题。我测试 20.04 和 22.04 版本没有问题，执行下面命令加载内核模块：\nsudo apt install linux-modules-extra-`uname -r` sudo modprobe binder_linux devices=\u0026#34;binder,hwbinder,vndbinder\u0026#34; sudo modprobe ashmem_linux 使用一键脚本安装 Docker3：\ncurl -sSL https://get.docker.com/ | sh 或者使用包管理器安装 Docker 以及 Docker Compose：\nsudo apt install docker docker-compose Arch Linux # 安装 linux-zen 内核：\n# 更新系统 sudo pacman -Syu # 安装 linux-zen 内核 sudo pacman -S linux-zen linux-zen-headers # 更新 GRUB 引导配置 sudo grub-mkconfig -o /boot/grub/grub.cfg # 重启系统 sudo reboot # 验证内核版本，可以看到内核带有 zen 字样 uname -r 使用包管理器安装 Docker 以及 Docker Compose：\nsudo pacman -S docker docker-compose NixOS # 安装 linux-zen 内核：\n# 编辑 /etc/nixos/configuration.nix 增加一行 boot.kernelPackages = pkgs.linuxPackages_zen # rebuild 更新配置 sudo nixos-rebuild switch # 重启系统 sudo reboot # 验证内核版本，可以看到内核带有 zen 字样 uname -r 安装 Docker 以及 Docker Compose：\n# 编辑 /etc/nixos/configuration.nix 添加一行 virtualisation.docker.enable = true; # docker-compose environment.systemPackages = with pkgs; [ docker-compose ]; # rebuild 更新配置 sudo nixos-rebuild switch 运行 Redroid 容器 # 直接启动 # sudo docker run -itd --rm --privileged \\ --pull always \\ -v ~/data:/data \\ -p 5555:5555 \\ redroid/redroid:11.0.0-latest 使用 Docker Compose 启动 # # docker-compose.yaml version: \u0026#34;3\u0026#34; services: redroid: stdin_open: true tty: true privileged: true pull_policy: always volumes: - ~/data:/data ports: - 5555:5555 image: redroid/redroid:11.0.0-latest # 启动 sudo docker-compose up -d 其他说明 # 连接设备 # 可以通过安装 adb4 和 scrcpy5 使用鼠标来操作安卓。\nmacOS 和 Linux 环境直接使用包管理器安装 android-platform-tools 和 scrcpy 即可：\n# macOS brew install --cask android-platform-tools scrcpy # Debian \u0026amp; Ubuntu sudo apt install android-platform-tools scrcpy # Arch Linux sudo pacman -S android-platform-tools scrcpy # NixOS environment.systemPackages = with pkgs; [ android-tools scrcpy ]; 如果你想使用网页进行操作，可以尝试 ws-scrcpy 这个项目。\n如果不想配置环境，可以使用我打包的镜像 aaronyes/ws-scrcpy。\n直接运行：\nsudo docker run --name ws-scrcpy -d -p 8000:8000 aaronyes/ws-scrcpy 或者使用 Docker Compose 启动：\nversion: \u0026#34;3\u0026#34; services: ws-scrcpy: container_name: ws-scrcpy ports: - 8000:8000 image: aaronyes/ws-scrcpy 启动后执行 adb 命令连接安卓：\nsudo docker exec ws-scrcpy adb connect ip:5555 如果使用 Docker Compose，可以把两个服务放在一起：\nversion: \u0026#34;3\u0026#34; services: redroid: container_name: redroid stdin_open: true tty: true privileged: true pull_policy: always volumes: - ~/data:/data ports: - 5555:5555 image: redroid/redroid:11.0.0-latest ws-scrcpy: container_name: ws-scrcpy ports: - 8000:8000 image: aaronyes/ws-scrcpy 这时可以直接使用容器名连接：\ndocker exec ws-scrcpy adb connect redroid:5555 没有应用商店怎么安装软件 # 可以使用 adb 安装，在电脑上下载好 apk 安装包，然后执行 adb install \u0026lt;apk路径\u0026gt;\n推荐安装一个 Via 浏览器，体积很小，轻量无广告，后续可以通过浏览器下载安装其他应用\n安装 Magisk # 可以参考下面这个地址：\nhttps://gist.github.com/assiless/a23fb52e8c6156db0474ee8973c4be66 GMS 支持 # 参考下面这个地址：\nhttps://github.com/remote-android/redroid-doc?tab=readme-ov-file#gms-support 参考文献 # https://viayoo.com/zh-cn/ https://github.com/NetrisTV/ws-scrcpy https://gist.github.com/assiless/a23fb52e8c6156db0474ee8973c4be66 https://github.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/remote-android/redroid-doc\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://docs.docker.com/engine/install/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://developer.android.com/tools/adb\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/Genymobile/scrcpy\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-07-17","externalUrl":null,"permalink":"/posts/2024/07/build-your-own-cloud-phone/","section":"所有文章","summary":"","title":"打造自己的云手机","type":"posts"},{"content":"","date":"2024-07-03","externalUrl":null,"permalink":"/tags/avif/","section":"标签","summary":"","title":"AVIF","type":"tags"},{"content":" 前言 # 在上一篇中，我介绍了 AVIF 格式以及它的使用方法。当时觉得这是一个非常有潜力的图片格式，压缩率很高，画质也很好。但在实际把博客图片替换为 AVIF 之后，我发现了不少兼容性问题，最终还是决定换回 WebP。\n这篇就来聊聊我为什么要换回 WebP。\n兼容性问题 # 先看 AVIF 在 Can I use 上的兼容性数据。\n可以看到，Edge 浏览器从 121 版本才开始支持 AVIF，而这个版本 2024 年才发布。虽然写这篇文章时已过去半年，但应该还有很多人没有更新到最新版浏览器。更麻烦的是手机端，大部分移动浏览器也是刚刚支持。比如 QQ 浏览器，如果在 QQ 或微信中打开博客，很有可能看不到图片。虽然我的博客图片并不多，但这也很影响阅读体验。\n其次是设备对 AV1 硬件解码的支持。在维基百科中可以查看各 CPU 和 SoC 的支持情况，这里列出一些常见的：\nPC 端 # Intel # 11 代及以后：从第 11 代 Tiger Lake 处理器开始，Intel 集成了 AV1 硬件解码支持。 AMD # Ryzen 6000 系列及以后：基于 Zen 3+ 架构，开始支持 AV1 硬件解码。 Apple Silicon # M3 及以后：M3、M3 Pro、M3 Max 开始支持 AV1 硬件解码1。 移动端 # Qualcomm # 从 Snapdragon 888 开始支持 AV1 硬件解码。 iOS # 从 A17 Pro 开始支持 AV1 硬件解码2。 吐槽 # 苹果果然是牙膏厂，2023 年底发布的芯片才支持 AV1 硬件解码。虽然可以通过软件解码来弥补，但前提是系统或浏览器已经更新支持。在不支持硬解的设备上走软件解码，会导致 CPU 占用过高、设备发热，在性能较弱的旧设备上还会出现卡顿。\nWebP 介绍 # WebP3 是由谷歌开发的一种图片格式，旨在加快图片加载速度。它的核心优势是体积更小，在质量相同的情况下，WebP 格式图像的体积比 JPEG 小约 40%，大约只有 JPEG 的三分之二，可以节省大量的服务器带宽和存储空间。\n同样在 Can I use 查看 WebP 的支持情况。\n相比 AVIF，WebP 的支持广泛得多。除了早已被微软放弃的 IE 浏览器（应该没有人在用了吧），主流浏览器全部支持。而且 WebP 已经存在多年，生态成熟，使用它基本不用担心兼容性问题。\n如何将图片转换为 WebP 格式 # 使用 FFmpeg4 即可完成转换：\n# JPEG → WebP ffmpeg -i input.jpg output.webp # PNG → WebP ffmpeg -i input.png output.webp 调整输出质量，使用 -qscale 参数（0–100，值越大质量越高，文件也越大）：\nffmpeg -i input.jpg -qscale 75 output.webp 调整压缩级别，使用 -compression_level 参数（0–6，值越大压缩率越高，速度越慢）：\nffmpeg -i input.jpg -compression_level 4 output.webp 组合使用，同时指定质量和压缩级别：\nffmpeg -i example.png -qscale 80 -compression_level 4 example.webp 参考文献 # https://zh.wikipedia.org/wiki/WebP https://en.wikipedia.org/wiki/AV1#Hardware https://www.reddit.com/r/AV1/comments/ytzwxx/list_of_cpusoc_with_av1_support https://www.apple.com/newsroom/2023/10/apple-unveils-m3-m3-pro-and-m3-max-the-most-advanced-chips-for-a-personal-computer/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.reddit.com/r/AV1/comments/16gyfyw/apple_a17_pro_finally_supports_av1_hardware/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://developers.google.com/speed/webp\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://ffmpeg.org\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-07-03","externalUrl":null,"permalink":"/posts/2024/07/avif-is-so-good-why-did-i-still-choose-webp/","section":"所有文章","summary":"","title":"AVIF 如此好，为什么我还是选择了 WebP","type":"posts"},{"content":"","date":"2024-07-03","externalUrl":null,"permalink":"/tags/ffmpeg/","section":"标签","summary":"","title":"FFmpeg","type":"tags"},{"content":"","date":"2024-07-03","externalUrl":null,"permalink":"/tags/webp/","section":"标签","summary":"","title":"WebP","type":"tags"},{"content":"","date":"2024-07-03","externalUrl":null,"permalink":"/categories/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/","section":"分类","summary":"","title":"技术实践","type":"categories"},{"content":" 前言 # 最近在折腾博客图片格式的时候，接触到了 AVIF1 这个比较新的图片格式。了解之后发现它在压缩率上相比传统的 JPEG 有着非常大的优势，于是做了一些研究，在这里记录一下。\n什么是 AVIF # AVIF 是一种基于 AV1 视频编码技术的图片格式。与 JPEG 类似，它使用有损压缩来减小文件大小，但不同之处在于，AVIF 在相同画质下可以把文件体积压缩得更小。\n该格式由开放媒体联盟（AOMedia）开发，该联盟由亚马逊、Netflix、谷歌和 Mozilla 等公司组成。文件使用 AV1（AOMedia Video 1）算法压缩，并以 HEIF 容器格式存储2。由于 AV1 压缩技术是免版税的3，所以无需支付任何许可费用即可使用。\n你可以在 Can I use 上查看 AVIF 的浏览器支持范围。\nAVIF 对比 JPEG # 与 JPEG 相比，AVIF 最明显的优势是文件尺寸大幅缩小。更小的文件意味着更少的带宽消耗和更快的加载速度，将网页中的 JPEG 替换为 AVIF 后，图像的数据消耗量可能会减少一半。\n颜色深度是 AVIF 优于 JPEG 的另一个方面。JPEG 仅支持 8 位颜色深度，而 AVIF 支持 HDR4，这意味着更丰富的色彩和更多的细节表现。\nNetflix 有一些很好的视觉示例，比较了相同图像分别压缩为 JPEG 和 AVIF 的效果。WebP 和 PNG 格式的其他比较也值得一看。\n如何使用 AVIF # 可以使用 FFmpeg5 将其他格式的图片转换为 AVIF 格式。\n简单的转换示例：\n# JPEG → AVIF ffmpeg -i input.jpg output.avif # PNG → AVIF ffmpeg -i input.png output.avif 调整输出质量，使用 -crf 参数（0-63，值越低质量越高）：\nffmpeg -i input.jpg -c:v libaom-av1 -crf 30 -b:v 0 output.avif 调整压缩级别，使用 -cpu-used 参数（0-8，值越大速度越快，但压缩率越低）：\nffmpeg -i input.jpg -c:v libaom-av1 -cpu-used 4 -crf 30 -b:v 0 output.avif 组合使用，同时指定质量和压缩级别：\nffmpeg -i example.png -c:v libaom-av1 -crf 30 -cpu-used 4 -b:v 0 example.avif 参考文献 # https://caniuse.com/?search=avif https://jakearchibald.com/2020/avif-has-landed/ https://www.lifewire.com/what-is-an-avif-file-5078731 https://www.ctrl.blog/entry/webp-avif-comparison.html https://netflixtechblog.com/avif-for-next-generation-image-coding-b1d75675fe4 https://en.wikipedia.org/wiki/AVIF\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://aomedia.org/av1-features/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://en.wikipedia.org/wiki/High_dynamic_range\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://ffmpeg.org\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-06-04","externalUrl":null,"permalink":"/posts/2024/06/what-is-avif-how-to-use-it/","section":"所有文章","summary":"","title":"什么是 AVIF，如何使用它","type":"posts"},{"content":"","date":"2024-06-01","externalUrl":null,"permalink":"/tags/disko/","section":"标签","summary":"","title":"Disko","type":"tags"},{"content":" 前言 # NixOS1 是一个声明式配置的系统，整个系统都可以使用声明式的方式来配置。2023 年刚接触 NixOS 的时候，我还不知道 Disko2，系统分区等操作还是需要通过手动执行一些命令来完成。使用了一段时间后我发现了 tmpfs as root 这种玩法，对于我这种有严重强迫症的人来说是极好的，于是把本地的设备都用上了 tmpfs as root。使用了一段时间体验非常好，想着把手里的服务器也换成 NixOS，于是就遇到了这个困扰我几个月的问题。\n由于我本地的设备都是 UEFI + systemd-boot 的组合，使用起来一直很正常。但云服务器一般都是 BIOS 启动的，systemd-boot 对于 BIOS 来说有一些问题3，最后选择了 BIOS + GRUB 的组合，这与我本地的配置大不相同。\n问题 # 当我执行 rebuild 后就会出现如下报错：\n... updating GRUB 2 menu... updating GRUB 2 menu... updating GRUB 2 menu... Failed to get blkid info (returned 512) for / on tmpfs at /nix/store/nvycxmg4g2q5jyqdxfvkgi95sqs48iw3-install-grub.pl line 212. warning: error(s) occurred while switching to the new configuration 在 GitHub 上搜索相关问题，尝试了很多次也没有解决。经过一个多月的摸索最终找到了解决办法。\n解决办法 # 编辑 hardware-configuration.nix 文件，添加如下代码：\nboot.loader.grub.enable = true; boot.loader.grub.efiSupport = true; boot.loader.grub.efiInstallAsRemovable = true; 保存后重新 rebuild，发现已经正常了。\nhttps://nixos.org\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/nix-community/disko\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/systemd/systemd/issues/25963\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-06-01","externalUrl":null,"permalink":"/posts/2024/06/nixos-bios-boot-using-disko-to-manage-partitions/","section":"所有文章","summary":"","title":"NixOS 中 BIOS 启动的系统使用 Disko 管理分区","type":"posts"},{"content":"","date":"2023-03-20","externalUrl":null,"permalink":"/tags/api/","section":"标签","summary":"","title":"API","type":"tags"},{"content":"","date":"2023-03-20","externalUrl":null,"permalink":"/tags/chatgpt/","section":"标签","summary":"","title":"ChatGPT","type":"tags"},{"content":"","date":"2023-03-20","externalUrl":null,"permalink":"/en/tags/reverse-engineering/","section":"Tags","summary":"","title":"Reverse Engineering","type":"tags"},{"content":"","date":"2023-03-20","externalUrl":null,"permalink":"/en/categories/technical-practice/","section":"Categories","summary":"","title":"Technical Practice","type":"categories"},{"content":" 前言 # 2023 年初，ChatGPT 火得一塌糊涂，但用起来有几个明显的痛点：Web 端体验不够灵活，没法接入自己的应用；官方 API1 按 Token 收费，对于重度使用者来说成本不低；而且 Web 端和 API 是两套完全独立的系统，ChatGPT Plus 订阅用户的 GPT-4 额度没法通过 API 消耗。\n于是我萌生了一个想法：能不能把 ChatGPT Web 端的接口逆向出来，包装成标准 OpenAI API 格式来用？这样既能用上 Web 端的无限额度，又能接入自己的工具链。\n这篇文章记录一下我从抓包分析到最终实现的完整过程。\n先搞清楚 ChatGPT Web 端的技术架构 # 动手之前，先用浏览器 DevTools 的 Network 面板把 ChatGPT Web 端的请求链路摸清楚。\n认证链路 # ChatGPT 使用 Auth02 作为 OAuth2 认证提供商。正常的 Web 登录流程是：\n浏览器 → chat.openai.com/auth/login → 跳转到 auth0.openai.com（Auth0 托管的登录页） → 用户输入邮箱密码 → Auth0 回调返回 access_token → 前端把 token 存到 session 中 但这个流程是给浏览器设计的，有多次 302 重定向、Cookie 传递、JavaScript 渲染。命令行工具没法直接走这个流程。\n对话链路 # 登录后，前端发消息用的接口长这样：\nPOST https://chat.openai.com/backend-api/conversation Authorization: Bearer \u0026lt;access_token\u0026gt; Content-Type: application/json Accept: text/event-stream 请求体：\n{ \u0026#34;action\u0026#34;: \u0026#34;next\u0026#34;, \u0026#34;messages\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;uuid\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;parts\u0026#34;: [\u0026#34;你好\u0026#34;] } } ], \u0026#34;model\u0026#34;: \u0026#34;text-davinci-002-render-sha\u0026#34;, \u0026#34;parent_message_id\u0026#34;: \u0026#34;uuid\u0026#34; } 响应是 SSE（Server-Sent Events）3 格式，逐字输出：\ndata: {\u0026#34;message\u0026#34;: {\u0026#34;content\u0026#34;:{\u0026#34;parts\u0026#34;:[\u0026#34;你\u0026#34;]}, ...}} data: {\u0026#34;message\u0026#34;: {\u0026#34;content\u0026#34;:{\u0026#34;parts\u0026#34;:[\u0026#34;你好\u0026#34;]}, ...}} data: [DONE] 这里有个关键差异：Web 端的对话是一棵消息树（每条消息有 parent_message_id，支持分支和重新生成），而 OpenAI API 是线性的 messages 数组。后面做协议转换的时候需要处理这个差异。\n逆向 Auth0 认证流程 # 这是整个过程中最核心也最有趣的一步。\n思路转变：从 Web 到 iOS # 一开始我尝试直接模拟浏览器的登录流程，但很快就发现行不通：Auth0 的登录页有大量反爬措施，JavaScript 校验、浏览器指纹检测、reCAPTCHA 都有。\n换个思路：移动端的认证流程通常比 Web 端简单。于是抓包分析了 ChatGPT 的 iOS 客户端，发现它也用 Auth0，但走的是 OAuth2 + PKCE（Proof Key for Code Exchange）4 扩展，不需要浏览器环境。\nPKCE 简述 # PKCE 是 OAuth2 的安全扩展，原本是为无法安全存储 client_secret 的移动端和桌面端应用设计的。流程很简单：\n客户端生成 code_verifier（随机字符串） 客户端计算 code_challenge = SHA256(code_verifier) 授权请求中带上 code_challenge 回调时带上原始的 code_verifier 服务端验证 SHA256(code_verifier) == code_challenge，发放 Token 好处是：即使授权码被截获，没有 code_verifier 也无法换取 Token。\n拆解出的认证流程 # 通过分析 iOS 客户端的网络请求，我把完整的认证流程拆解成了 7 步：\n获取 preauth_cookie 拼装 authorize URL，模拟 iOS 客户端参数 访问 authorize URL，提取 state 参数并保存 Cookie 提交邮箱 提交密码 处理回调或 MFA 验证 如果需要 MFA，提交验证码后回到第 6 步 最终用授权码换取 Access Token 有几个值得注意的细节：\ncode_verifier 为什么可以硬编码？ 因为 iOS 客户端是可以反编译的，code_verifier 和 code_challenge 这对值写死在客户端里，所有 iOS 用户共用同一对。PKCE 在这种场景下保护的是传输链路（授权码泄露不会导致 Token 泄露），而不是客户端本身。\nclient_id 是哪来的？ 同样来自 iOS 客户端反编译。这是 OpenAI 在 Auth0 注册的 iOS 应用 ID。\nredirect_uri 为什么是 com.openai.chat://...？ 这是 iOS 的 URL Scheme，用于 Auth0 授权完成后跳回 App。在我们的实现里不需要真正跳转，只需要从响应的 Location 头里提取 code 参数。\nPython 实现大致长这样：\nclass Auth0: def auth(self, login_local=False) -\u0026gt; str: return self.__part_one() if login_local else self.get_access_token_proxy() def __part_one(self): # Step 1: get preauth def __part_two(self): # Step 2: build authorize URL def __part_three(self): # Step 3: follow authorize def __part_four(self): # Step 4: submit email def __part_five(self): # Step 5: submit password def __part_six(self): # Step 6: handle callback/MFA def __part_seven(self): # Step 7: MFA OTP def get_access_token(self): # Final: code → token 实现 SSE 流式对话代理 # 拿到 Access Token 后，下一步是调 ChatGPT 的对话接口。\n请求构造比较直观：每条消息需要一个 UUID 作为 id，parent_message_id 指向上一条消息形成对话链，首次对话不带 conversation_id，服务端会创建并返回。action 可以是 next（新消息）、variant（重新生成）、continue（继续输出）。\n难点在于 SSE 响应的处理。Python 的 Flask 是同步框架，但 SSE 需要异步消费流式响应。我的解决方案是异步线程 + 阻塞队列 + Generator 桥接：\ndef _request_sse(self, url, headers, data): queue, event = block_queue.Queue(), threading.Event() t = threading.Thread(target=asyncio.run, args=(self._do_request_sse(url, headers, data, queue, event),)) t.start() return queue.get(), queue.get(), self.__generate_wrap(queue, t, event) 为什么要绕这么一圈？因为 httpx5 的流式 API 是 async 的（async with client.stream('POST', url) 需要在 async 上下文中），但上层是同步代码（Flask 的路由处理函数、CLI 的 readline 循环都是同步的），又不想把全局架构从 Flask 改成 aiohttp/uvicorn，改动太大。\n所以用一个线程跑异步事件循环，通过 queue.Queue 把异步世界的数据搬运到同步世界，对外暴露一个标准 Generator，上层代码完全无感。\n还有一个细节：threading.Event 用于中断保护。如果客户端断开连接触发了 GeneratorExit，Event 被置位，异步线程检测到后主动关闭 httpx 连接，避免线程泄漏。\nWeb API 到 OpenAI API 的协议转换 # 这是把 ChatGPT Web 接口包装成标准 OpenAI API 的关键步骤。两种 API 格式差异很大：\n维度 ChatGPT Web API OpenAI Public API 认证 Bearer access_token Bearer sk-xxx（API Key） 请求格式 消息树（parent_message_id） messages 数组 响应格式 SSE + 消息树节点 SSE + choices 数组 会话管理 服务端存储 conversation_id 无状态 请求转换 # 我的做法是维护一个本地的消息树，把 OpenAI 格式的 messages 数组转换成树形结构，支持多轮对话和 regenerate：\ndef talk(self, content, model, message_id, parent_message_id, ...): if conversation_id: parent = conversation.get_prompt(parent_message_id) else: parent = conversation.add_prompt(Prompt(parent_message_id)) parent = conversation.add_prompt(SystemPrompt(self.system_prompt, parent)) conversation.add_prompt(UserPrompt(message_id, content, parent)) user_prompt, gpt_prompt, messages = conversation.get_messages(message_id, model) 响应转换 # Web 端每次返回的是全量文本（parts[0] 越来越长），而 OpenAI API 返回的是增量文本。需要做差值计算：\n# Web 端返回 {\u0026#34;message\u0026#34;: {\u0026#34;content\u0026#34;: {\u0026#34;parts\u0026#34;: [\u0026#34;完整文本\u0026#34;]}, \u0026#34;author\u0026#34;: {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;}}} # 转换为 OpenAI 格式 data: {\u0026#34;choices\u0026#34;: [{\u0026#34;delta\u0026#34;: {\u0026#34;content\u0026#34;: \u0026#34;增量文本\u0026#34;}, \u0026#34;finish_reason\u0026#34;: null}]} data: {\u0026#34;choices\u0026#34;: [{\u0026#34;delta\u0026#34;: {}, \u0026#34;finish_reason\u0026#34;: \u0026#34;stop\u0026#34;}]} data: [DONE] Token 超限裁剪 # OpenAI API 有 Token 上限（gpt-3.5-turbo 是 4096，gpt-4 是 8192）。对话历史过长时需要本地裁剪：\ndef __reduce_messages(self, messages, model, token=None): max_tokens = self.FAKE_TOKENS[model] if self.__is_fake_api(token) else self.MAX_TOKENS[model] while gpt_num_tokens(messages) \u0026gt; max_tokens - 200: if len(messages) \u0026lt; 2: raise Exception(\u0026#39;prompt too long\u0026#39;) messages.pop(1) # 从第 2 条开始删，保留 system prompt 和最新对话 return messages 裁剪策略：保留 messages[0]（system prompt）和最新的几轮对话，从最早的用户消息开始删。- 200 是给模型回复留的余量。\n从技术验证到实际交付 # 接口跑通之后，接下来的问题是怎么让身边的同事和朋友也能用上。\n批量注册 # 接口跑通之后，实际用起来发现 ChatGPT 有单账号频率限制，请求量一大就报错。要解决这个问题，最直接的办法就是多账号。于是我写了个注册机，使用自己的域名邮箱批量注册了 200 个 ChatGPT 账号。其中两个开了 Plus 订阅（只有 Plus 才能用 GPT-4），费用跟几个朋友均摊。剩下的账号都走免费的 GPT-3.5，日常使用完全够用。\nToken 管理与持久化 # Access Token 有效期 14 天，到期后需要重新认证刷新。我把所有账号的 Token 存到了 PostgreSQL 数据库中，写了个定时任务自动检测过期时间并批量刷新，保证 Token 池始终可用。\n负载均衡 # 200 个账号不能只用一个。我在代理服务层加了一层简单的负载均衡：每次请求过来，从数据库中轮询选取一个可用的 Token 去调用 ChatGPT 接口。这样既避免了单账号频率限制，也分散了各账号的请求压力。\n最终的效果是：对外暴露一个标准的 OpenAI API 地址，同事和朋友只需要在自己的应用里把 API Base URL 指向我的服务就行，完全感知不到背后是 200 个账号在轮转。GPT-4 的请求路由到 Plus 账号池，GPT-3.5 的请求路由到免费账号池。\n参考文献 # https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce https://platform.openai.com/docs/api-reference\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://auth0.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://datatracker.ietf.org/doc/html/rfc7636\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.python-httpx.org\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2023-03-20","externalUrl":null,"permalink":"/posts/2023/03/chatgpt-web-to-api/","section":"所有文章","summary":"","title":"把 ChatGPT Web 包装成标准 API 的逆向实践","type":"posts"},{"content":"","date":"2023-03-20","externalUrl":null,"permalink":"/tags/%E9%80%86%E5%90%91/","section":"标签","summary":"","title":"逆向","type":"tags"},{"content":"","date":"2023-01-17","externalUrl":null,"permalink":"/tags/fastapi/","section":"标签","summary":"","title":"FastAPI","type":"tags"},{"content":"","date":"2023-01-17","externalUrl":null,"permalink":"/tags/jwt/","section":"标签","summary":"","title":"JWT","type":"tags"},{"content":" 前言 # 在开发 Web 应用时，用户认证是一个避不开的话题。最近在用 FastAPI 做后端项目的时候，接触到了 JWT1 这种认证方式，觉得挺有意思的，于是做了一些研究，在这里记录一下。\nJWT 是什么 # JWT 的全称是 JSON Web Token，简单来说它是一种令牌格式。令牌的内容是编码后的 JSON，且常用在 Web 领域的身份校验，所以称之为 JWT。\n一般来说，用户先用自己的用户名和密码（OAuth 之类的第三方认证暂时略去）发送到服务器，服务器验证通过后给用户签发一个 Token。这个 Token 中包含了必要的信息，比如签发人、主题（一般是用户 ID）、有效时间等。之后服务器便不再需要用户名和密码，单靠这个 Token 就可以确认用户的身份。\nJWT 解决了什么问题 # 很多人可能会问：既然用户已经有了用户名和密码，何必还要多此一举？先生成 Token 再使用，而不是直接用用户名密码？就好像 HTTP Basic Authentication2 那样，简单粗暴，靠 HTTPS 也能保证基本的安全。\n主要有两个原因：第一个是安全，第二个是压力。\n安全比较好理解。与经常把明文的用户名和密码放在请求里传输相比，可以带有失效机制的 JWT 显然更加安全。\n第二个原因我觉得更为主要，那就是压力。我们不妨先想想用户名和密码的鉴权过程是什么样的：\n用户发出请求，在 HTTP 请求中带上 Base64 编码后的用户名和密码。 服务器解码请求，将用户名、密码与数据库中的内容（一般密码部分只存 Hash）作比对，返回通过或不通过。 如果每秒只有几个请求，这样的设计当然没问题。但如果这是一个每秒请求量数万的服务，仅做鉴权校验就会让数据库承受非常大的压力。而且目前相对成熟的数据库往往是单点的（涉及到事务的东西，即使能扩容也要脱一层皮）。\n那有没有可能，我们签发一个令牌给用户，以后不管是本服务器的其他副本（Replica），还是跨服务器，都能进行验证，而且验证的过程不需要查数据库？这样的话，服务器就真正变成了无状态的。熟悉分布式系统的小伙伴都知道，无状态可太省事了。\nJWT 的安全性是如何保障的 # 既然就是 Base64 编码过的 JSON，那用户自己捏一个骗服务器说这是 Token 可以吗？\n当然不可以。JWT 会进行数字签名，最后一部分会使用服务器独有的密钥和前两部分的内容一起生成签名。由于这个密钥只有服务器才有，而且通过内容也几乎不可能反推出密钥，所以攻击者无法伪造 JWT。\nJWT 的结构详解 # JWT 的结构分为三块：\n头部 Header 载荷 Payload 签名 Signature 三个部分都是 Base64 编码的字符串，用 . 连接起来。一个典型的 JWT 如下所示：\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQ 其中头部解码后为：\n{ \u0026#34;alg\u0026#34;: \u0026#34;HS256\u0026#34;, \u0026#34;typ\u0026#34;: \u0026#34;JWT\u0026#34; } 载荷解码后为：\n{ \u0026#34;sub\u0026#34;: \u0026#34;1234567890\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;John Doe\u0026#34;, \u0026#34;iat\u0026#34;: 1516239022 } 最后一部分是签名，用于校验前两部分是否被篡改。\n如何使用 JWT # HTTP Header # 理论上，只要把 JWT 传给服务器就行了。RFC 75193 也没有规定必须在哪个位置使用 JWT。但考虑到它是个 Token，一般的使用方式是放在 Request Header 里：\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQ 以 FastAPI 为例 # 这里用的是 python-jose4，直接 pip install \u0026quot;python-jose[cryptography]\u0026quot; 即可。也可以使用其他库，详见 https://jwt.io/libraries。\n出于简洁考虑，这里略去 FastAPI 的框架代码，只展示核心部分。\n首先创建一个 Endpoint 来签发 Token：\nfrom fastapi.security import HTTPBasic, HTTPBasicCredentials http_basic = HTTPBasic() def create_jwt_access_token( data: dict, expires_delta: timedelta, ) -\u0026gt; str: to_encode = data.copy() to_encode.update({\u0026#34;exp\u0026#34;: datetime.utcnow() + expires_delta}) return jwt.encode( to_encode, \u0026#34;jwt_secret\u0026#34;, # 请替换为你自己的密钥 algorithm=\u0026#34;HS256\u0026#34;, ) @app.post(\u0026#34;/auth/issue-new-token\u0026#34;) def issue_new_token( credentials: HTTPBasicCredentials = Depends(http_basic), ): username = basic_authentication(credentials) # 验证用户名密码 access_token = create_jwt_access_token( data={\u0026#34;sub\u0026#34;: username}, expires_delta=timedelta(seconds=1234), ) return {\u0026#34;access_token\u0026#34;: access_token, \u0026#34;token_type\u0026#34;: \u0026#34;bearer\u0026#34;} 这是一个极简版的 JWT 生成接口。需要注意的是，这里既没有 Rate Limit，也没有针对错误进行处理，只是一个基本的演示。\n接下来是验证 JWT 的函数：\nfrom fastapi.security import HTTPBearer, HTTPAuthorizationCredentials http_bearer = HTTPBearer() def jwt_authentication( credentials: HTTPAuthorizationCredentials = Depends(http_bearer), ): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=\u0026#34;Could not validate credentials\u0026#34;, headers={\u0026#34;WWW-Authenticate\u0026#34;: \u0026#34;Bearer\u0026#34;}, ) try: if credentials.scheme.lower() != \u0026#34;bearer\u0026#34;: raise credentials_exception return jwt.decode( credentials.credentials, \u0026#34;jwt_secret\u0026#34;, # 请替换为你自己的密钥 algorithms=[\u0026#34;HS256\u0026#34;], ) except JWTError: raise credentials_exception 在需要认证的接口中加入依赖即可：\n# 方式一：只验证，不获取 payload @app.get(\u0026#34;/api/protected\u0026#34;, dependencies=[Depends(jwt_authentication)]) def protected_api(): ... # 方式二：获取 payload 做进一步处理 @app.get(\u0026#34;/api/protected\u0026#34;) def protected_api(jwt_payload: dict = Depends(jwt_authentication)): # 处理 jwt_payload ... 前端使用思路 # 由于很多组件都会依赖后端的 API，且这些 API 需要 JWT，所以一般会用 Context 来管理 JWT，以便于跨组件传递。\n在制作 AuthStateContext.Provider 的时候，我的思路是：\n由于用了 OAuth，JWT 会从后端通过 query string 传入，首先检查 query string 有没有新的 Token 如果有，提取出来存到 localStorage 如果没有，进入下一步 检查 localStorage 里有没有存储的 Token 如果有，检查是否有效 如果没有，或上一步的结果为无效，进入下一步 调用 \u0026ldquo;who am I\u0026rdquo; 接口，让后端校验 JWT（这一步可选，但可以增加可靠性） 如果所有校验都通过，将 JWT 状态通过 Provider 传递给其他组件 否则跳转到相应页面提示用户 其他组件使用 useContext(AuthStateContext) 即可获取已验证的 Token。\nJWT 的几个缺陷 # JWT 还是有几个小缺陷的，在设计系统时需要考虑：\nJWT 一旦签发就难以撤回，这是无状态带来的代价。 考虑到第一点，往往需要设置较短的有效期，客户端需要维护 Token 的刷新。 JWT 中的信息默认不加密，任何人都可以读取，所以不要把敏感信息放在里面。 JWT 推荐在 HTTPS 环境下使用。 参考资料 # https://en.wikipedia.org/wiki/JSON_Web_Token https://jwt.io/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://datatracker.ietf.org/doc/html/rfc7519\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/mpdavis/python-jose\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2023-01-17","externalUrl":null,"permalink":"/posts/2023/01/jwt-introduction/","section":"所有文章","summary":"","title":"JWT 简介与实践","type":"posts"},{"content":"","date":"2022-03-23","externalUrl":null,"permalink":"/en/categories/blog/","section":"Categories","summary":"","title":"Blog","type":"categories"},{"content":"","date":"2022-03-23","externalUrl":null,"permalink":"/tags/font/","section":"标签","summary":"","title":"Font","type":"tags"},{"content":"","date":"2022-03-23","externalUrl":null,"permalink":"/categories/%E5%8D%9A%E5%AE%A2/","section":"分类","summary":"","title":"博客","type":"categories"},{"content":" 前言 # 最近一直在使用「霞鹜文楷」1 这款开源字体，十分喜欢，便想着把博客的字体也换成霞鹜文楷。\n配置 # 阅读字体的 [Issue #24]2，发现已经有大佬通过 ttf2woff2 工具转换好了字体，并提供了在网页上使用霞鹜文楷字体的方法。经过测试，如果直接使用 lxgw-wenkai-webfont 在 Safari 上显示会不正常，使用 lxgw-wenkai-lite-webfont 就不会出现问题。lite 版本精简了部分字重，在兼容性上表现更好。\n通过搜索 3 得知 Congo 主题允许直接将额外的代码插入到模板的 和 部分，只需要创建 layouts/partials/extend-head.html 或 layouts/partials/extend-footer.html 文件即可。创建好 html 文件，将下面的代码贴入其中，保存并刷新网页，博客就已经换好霞鹜文楷字体了。\n\u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdn.jsdelivr.net/npm/lxgw-wenkai-lite-webfont@1.1.0/style.css\u0026#34; /\u0026gt; \u0026lt;style\u0026gt; body { font-family: \u0026#34;LXGW WenKai Lite\u0026#34;, sans-serif; } \u0026lt;/style\u0026gt; https://github.com/lxgw/LxgwWenKai\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/chawyehsu/lxgw-wenkai-webfont\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://jpanther.github.io/congo/docs/partials/#head-and-footer\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2022-03-23","externalUrl":null,"permalink":"/posts/2022/03/change-the-font-of-the-blog/","section":"所有文章","summary":"","title":"为博客更换字体","type":"posts"},{"content":"","date":"2022-02-11","externalUrl":null,"permalink":"/tags/hugo/","section":"标签","summary":"","title":"Hugo","type":"tags"},{"content":" 前言 # 似乎每年都可以看到类似于\u0026quot;xx 年，还有必要建立自己的博客吗\u0026quot;的讨论。这确实是个值得思考的问题，如今已经是 2022 年了，有着很多成熟的写作与发布平台，对于我来说，它们有利有弊，虽然可以很方便地在上面撰写文章，但也有着各种局限性。\n我第一次开始写博客是在 2015 年，当时用着免费空间部署了自己的第一个网站。2017 年，购买了人生中第一台服务器，接触了 Typecho1 博客系统，购买了 Handsome2 博客主题。2018 年，写作平台更换到了微信公众号，凭借着一周五更、无广告、高质量，吸引了不少读者，阅读占比也十分高。当时的我充满热情，坚持了三年，写了有 700 多篇文章。随着时间的推移，我认为这是一个受各种限制的平台，加上腾讯的各种整活，在 2021 年，我注销了它。\n如今我决定重启博客。自 2021 年注销公众号以来，我变得越来越压抑。我的朋友很少，非必要情况下我从来不会主动去联系他们。我意识到自己逐渐失去了表达能力，这可不是好事。我便想试着通过写作，来找回曾经那个充满热情的自己。\n选择 # 博客分为两种：动态博客与静态博客。\n我使用过的动态博客程序：\nWordPress: https://wordpress.com Typecho: https://typecho.org Halo: https://www.halo.run 我使用过的静态博客程序：\nGridea: https://open.gridea.dev Hexo: https://hexo.io Hugo: https://gohugo.io 这些都是很不错的博客程序，它们各有优缺点。动态博客功能丰富、上手简单，但需要维护服务器和数据库；静态博客轻量快速、部署方便，但功能上相对有限。\n开始 # 尝试过多种不同的博客程序后，这次选择了 Hugo。在它的官网3上可以找到四种常见操作系统的安装步骤。\n创建站点 # 在 Hugo 中创建网站文件夹的命令是 hugo new site 网站名字，比如这里我创建一个名为 Blog 的博客文件夹。\nhugo new site Blog cd Blog 这时就可以输入 hugo server 尝试访问默认生成的网页了。\nhugo server Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) Press Ctrl+C to stop 安装主题 # Hugo 有很多不错的主题，可以在 https://themes.gohugo.io 挑选。主题的 Github 仓库或对应的演示站点通常会有安装教程以及配置修改教程，按照说明操作即可。\n撰写文章 # 可以使用命令创建一篇名为 Hello World 的文章。\nhugo new posts/hello-world/index.md hugo server -D 这时你可以访问 http://localhost:1313/posts/hello-world 查看。你可能会发现启动参数加了一个 -D，这是用来预览草稿的参数。新创建的文章默认是 draft: true，除非手动把 true 修改为 false，否则这篇文章不仅在本地无法预览，发布后也不会显示。\nHugo 使用 Markdown4 标记语言来撰写文章，花费 10 分钟就可以学会使用。\n部署 # Hugo 可以部署在很多地方，官网上也有部署教程5，很多都是免费的，门槛非常低。其中我认为托管到 GitHub Pages 和 Cloudflare Pages 是最方便的。\nhttps://typecho.org\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.ihewro.com/archives/489\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://gohugo.io/installation\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.runoob.com/markdown/md-tutorial.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://gohugo.io/hosting-and-deployment\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2022-02-11","externalUrl":null,"permalink":"/posts/2022/02/how-i-built-my-personal-blog/","section":"所有文章","summary":"","title":"我是如何创建个人博客的?","type":"posts"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":" 如果本网站的任何内容无意中侵犯了您的版权或利益，请及时与我联系，我将采取适当行动。 除非另有特殊说明，否则 本站 所有内容均在 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 许可下授权。\n以下是该 许可证 的人类可读摘要（而不是替代）。\n您可以自由地： # 共享 — 在任何媒介以任何形式复制、发行本作品 演绎 — 修改、转换或以本作品为基础进行创作 只要你遵守许可协议条款，许可人就无法收回你的这些权利。\n惟须遵守下列条件： # 署名 — 您必须给出 适当的署名，提供指向本许可协议的链接，同时 标明是否（对原始作品）作了修改。您可以用任何合理的方式来署名，但是不得以任何方式暗示许可人为您或您的使用背书。 非商业性使用 — 您不得将本作品用于 商业目的。 相同方式共享 — 如果您再混合、转换或者基于本作品进行创作，您必须基于 与原先许可协议相同的许可协议 分发您贡献的作品。 没有附加限制 — 您不得适用法律术语或者 技术措施 从而限制其他人做许可协议允许的事情。 声明： # 您不必因为公共领域的作品要素而遵守许可协议，或者您的使用被可适用的 例外或限制 所允许。 不提供担保。许可协议可能不会给与您意图使用的所必须的所有许可。例如，其他权利比如 形象权、隐私权 或 人格权 可能限制您如何使用作品。 署名-非商业性使用-相同方式共享 4.0 国际许可证\n","externalUrl":null,"permalink":"/copyright/","section":"Hideaway","summary":"","title":"版权声明","type":"page"},{"content":"👋 你好呀，我是 Aaron。\n","externalUrl":null,"permalink":"/about/","section":"Hideaway","summary":"","title":"关于我","type":"page"},{"content":" 介绍 # 本隐私政策旨在帮助您了解我们对可能从您那里收集的或您提供给我们的任何信息的做法，我们使用这些信息的方式，以及我们如何处理这些信息。\n鉴于我们不收集任何个人数据，我们的做法直截了当，并致力于保护您的隐私。\n信息处理 # 本站是一个非商业性质的个人独立博客站点，我们致力于保护用户隐私。根据这一点，我们确认，在您访问我们的网站时，我们不会收集、存储或处理您的任何个人数据。\n个人数据是指有可能识别你个人的任何信息。由于我们不收集此类信息，所以我们没有可能使用、分享或出售这些数据。\n安全声明 # 虽然我们不收集个人数据，但我们非常重视您在提供任何非个人数据方面的信任，因此我们尽全力使用可接受的方法来保护这些数据、维护我们所使用服务在物理层面或软件层面的安全性。然而，没有任何一种互联网传输方法或电子存储方法是 100% 安全和可靠的，我们不能保证其绝对安全。\n政策变更 # 我们可能会不时地更新我们的隐私政策。因此，我们建议你定期查看本页面以了解任何变化。我们将通过在本页面上发布新的隐私政策来通知您任何变化。这些变化在本页面上公布后立即生效。\n","externalUrl":null,"permalink":"/privacy/","section":"Hideaway","summary":"","title":"隐私政策","type":"page"}]