Harness Engineering 精读笔记

基于 2025.11 — 2026.03 期间 Anthropic、OpenAI、LangChain、Google DeepMind、Stanford 发布的九篇核心文章的精读整理。 目标:理解 Harness Engineering 是什么,以及大公司和学术机构在实践中做了哪些探索。 一、什么是 Harness Engineering? 1.1 Harness 的定义 LangChain 给出了最清晰的公式化定义: Agent = Model + Harness “If you’re not the model, you’re the harness.” “A harness is every piece of code, configuration, and execution logic that isn’t the model itself.” 具体来说,Harness 包括: System Prompts — 系统提示词,定义 Agent 的行为边界 Tools / Skills / MCPs — 工具集及其描述(文件读写、代码执行、浏览器操作等) Bundled Infrastructure — 捆绑基础设施(文件系统、沙箱、浏览器) Orchestration Logic — 编排逻辑(子 Agent 派生、任务交接、模型路由) Hooks / Middleware — 确定性执行逻辑(压缩、续行、lint 检查等) Anthropic 不给 Harness 下抽象定义,而是通过实践来展示:他们用 “harness design” 和 “harnesses for long-running agents” 来描述围绕 Agent 执行循环的编排层。Anthropic 在博客中明确说:“harness design is key to performance at the frontier of agentic coding”。 ...

May 2, 2026

工厂模式

定义 工厂模式(Factory Pattern)是一种创建型设计模式,它提供了一种创建对象的方式,将对象的创建逻辑与使用逻辑分离。工厂模式有三种变体:简单工厂模式、工厂方法模式和抽象工厂模式。 为什么需要工厂模式 工厂模式要解决的核心问题是:将对象的创建过程与使用过程分离。 问题场景:假设我们正在开发一个网络层,需要创建URLSession对象。 直接使用构造函数创建对象看起来很简单: class NetworkManager { func fetchData(from url: URL) { // 直接创建URLSession let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 30 config.httpAdditionalHeaders = ["Authorization": "Bearer xxx"] let session = URLSession(configuration: config) session.dataTask(with: url) { data, response, error in // 处理响应... }.resume() } } 这种方式有什么问题? 创建逻辑与业务逻辑混在一起:fetchData方法既要负责创建session,又要负责发起请求,职责不单一 创建逻辑无法复用:如果另一个方法也需要相同配置的session,只能复制这段创建代码 难以替换实现:如果想在测试时使用Mock的session,或者在不同环境使用不同配置,需要修改业务代码 隐藏了依赖关系:从方法签名看不出这个类依赖URLSession,依赖关系被隐藏在方法内部 工厂模式的解决思路: 工厂模式的核心是:调用方不再自己new对象,而是向工厂"要"对象。 // 工厂负责创建 class URLSessionFactory { static func createDefault() -> URLSession { let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 30 config.httpAdditionalHeaders = ["Authorization": "Bearer xxx"] return URLSession(configuration: config) } static func createBackground(identifier: String) -> URLSession { let config = URLSessionConfiguration.background(withIdentifier: identifier) return URLSession(configuration: config) } } // 业务代码只负责使用 class NetworkManager { private let session: URLSession init(session: URLSession = URLSessionFactory.createDefault()) { self.session = session } func fetchData(from url: URL) { session.dataTask(with: url) { data, response, error in // 处理响应... }.resume() } } 这样做的好处: ...

May 2, 2026

KVO底层原理

基础概念 KVO(Key-Value Observing,键值观察)是 Apple 基于 KVC 实现的一种观察者模式,允许对象监听另一个对象特定属性的变化。当被观察对象的属性值发生改变时,观察者会收到通知。 // 注册观察 [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; // 接收通知 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context { if ([keyPath isEqualToString:@"name"]) { NSLog(@"name changed: %@ → %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]); } } // 移除观察 - (void)dealloc { [person removeObserver:self forKeyPath:@"name"]; } KVO 的底层实现原理:isa-swizzling KVO 的核心实现机制是 isa-swizzling(isa 指针替换)。当对象被添加观察者时,Runtime 会在运行时动态创建该对象所属类的一个子类,并将对象的 isa 指针指向这个新的子类。 完整流程 flowchart TD A["addObserver:forKeyPath:"] --> B["Runtime 动态创建子类NSKVONotifying_ClassName"] B --> C["重写被观察属性的 setter 方法"] B --> D["重写 class 方法返回原始类"] B --> E["重写 dealloc 方法"] B --> F["重写 _isKVOA 方法返回 YES"] C --> G["将对象的 isa 指向NSKVONotifying_ClassName"] G --> H["当属性值改变时"] H --> I["调用 willChangeValueForKey:"] I --> J["调用原始 setter 方法"] J --> K["调用 didChangeValueForKey:"] K --> L["通知所有观察者"] 第一步:动态创建子类 当首次对某个类的实例调用 addObserver:forKeyPath:options:context: 时,Runtime 会: ...

May 2, 2026

iOS国际化

前言 很多团队对"国际化"的理解停留在"把中文文案翻译成英文",等真正把 App 推向多区域时就会发现远远不够:阿拉伯语用户看到的界面左右颠倒了,用户在纽约和东京看到的"今天"不是同一天,欧洲的小数点变成了逗号,日本用户抱怨价格里的¥符号指向人民币而不是日元,印度用户发现自己的 ₹1,23,456.78 被错误地按千分位显示成 ₹123,456.78,复数规则在俄语里比英语复杂得多,而这些问题单靠"翻译"都解决不了。 国际化(Internationalization,简称 i18n,取首末字母与中间 18 个字符)是架构层面的能力建设,让 App 能够适配不同语言、地区、文字方向、日历、时区、货币、单位制与文化习俗;而本地化(Localization,l10n)是针对某个具体地区的"适配落地"。两者的关系是:“国际化一次,本地化 N 次”。 本文从 NSLocale/Locale 的基础模型开始,把 iOS 国际化的完整工程面梳理清楚——文案本地化、复数/性别规则、RTL 布局、多时区、多币种、单位与数字格式、图片资源、字体、App 内切换语言、测试方法,最后给出研发规范与排查清单。 一、基础概念:Locale、Language 与 Region 1.1 Locale 不等于 Language 大多数工程师第一次接触国际化时会把"语言"和"地区"混为一谈,这在 iOS 下会直接导致格式错误。 Language(语言):zh、en、ar、ja…决定 UI 文字内容。 Region(地区):CN、US、SA、JP…决定格式(日期、数字、货币、时间制 12/24h、起始星期、度量单位)。 Locale(语言+地区):两者合成一个完整的标识,如 zh_CN、en_US、ar_SA、en_IN。 一个美国人移居日本后,完全可能把 iPhone 语言设为英文,但地区设为日本。此时: 场景 期望 UI 文案 英文 日期格式 1月23日(月) 风格?No,应显示 January 23 (Mon),因为语言决定月/星期的名字 数字分隔符 , 作千分位(与日本/美国一致) 货币符号 ¥(JPY) 温度单位 ℃(日本) 首日周 周日(日本区域) 对应的 Locale 是 en_JP——看似奇怪,但在现实中非常普遍。代码里要区分"用哪个语言去查字符串"和"用哪个 Locale 去格式化数据": let preferredLanguage = Locale.preferredLanguages.first ?? "en" let currentLocale = Locale.current let formatter = DateFormatter() formatter.locale = currentLocale formatter.dateStyle = .long print(formatter.string(from: Date())) 不要传 Locale(identifier: "en") 去格式化数字,那会丢掉用户的区域偏好。 ...

May 2, 2026

Function Calling与MCP

LLM很聪明,但它被困在一个"文本沙箱"里——它只能接收文本、输出文本。要让LLM调用API、查数据库、操作文件,就需要一个"桥梁"协议来连接LLM与外部世界。 Function Calling和**MCP(Model Context Protocol)**就是这样的桥梁。它们的目标相同——让LLM能调用外部工具——但设计理念和使用场景有很大不同。本文将详细对比这两种方案。 一、Function Calling:LLM原生的工具调用 1.1 基本原理 Function Calling是LLM提供商(OpenAI、Anthropic、Google等)在模型层面支持的工具调用能力。核心思路是: 你告诉LLM有哪些函数可用(函数名、参数定义、功能描述) LLM根据用户的问题判断是否需要调用函数 如果需要,LLM输出一个结构化的函数调用请求(而不是自然语言) 你的代码执行这个函数,把结果返回给LLM LLM基于结果生成最终回答 sequenceDiagram participant U as 用户 participant App as 应用程序 participant LLM as LLM API participant Tool as 外部工具/API U->>App: "北京今天天气怎么样?" App->>LLM: 用户消息 + 可用函数定义 LLM->>App: 函数调用请求get_weather(city="北京") App->>Tool: 调用天气API Tool->>App: {"temp": 22, "weather": "晴"} App->>LLM: 函数执行结果 LLM->>App: "北京今天天气晴朗,气温22°C" App->>U: 最终回答 关键点:LLM自身并不执行函数,它只是生成"我想调用这个函数"的结构化指令,真正的执行由应用程序完成。 1.2 函数定义 以OpenAI的格式为例,开发者需要用JSON Schema描述每个可用的函数: { "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市的当前天气信息", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如'北京'、'上海'" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位" } }, "required": ["city"] } } } ] } LLM通过阅读这个定义来理解:这个函数叫什么、干什么用、需要什么参数。description字段的质量直接影响LLM能否正确选择和调用函数。 ...

May 2, 2026

迪米特法则

什么是迪米特法则? 迪米特法则(Law of Demeter, LoD)也称为最少知识原则(Least Knowledge Principle, LKP),由 Ian Holland 在 1987 年提出。 Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. 每个单元应该只对与其紧密相关的单元有有限的了解。 更通俗的表述是:只和直接朋友交流,不要和陌生人说话。 核心思想 什么是"直接朋友"? 一个对象的"直接朋友"包括: 当前对象本身(this/self) 作为参数传入的对象 当前对象创建的对象 当前对象的成员变量 全局对象(如单例,技术上可访问,但应谨慎依赖) class Person { let wallet: Wallet // 成员变量 - 直接朋友 func buyItem(shop: Shop, item: Item) { // 参数 - 直接朋友 let receipt = Receipt() // 自己创建的对象 - 直接朋友 // 以下都是与直接朋友交流 let money = wallet.getMoney() shop.sell(item: item) receipt.record(item: item) } } 什么是"陌生人"? 如果需要通过多层对象去访问另一个对象,那个被间接访问的对象就是"陌生人"。 ...

May 2, 2026

外观模式

定义 外观模式(Facade Pattern)是一种结构型设计模式,它为子系统中的一组接口提供一个统一的高层接口。外观模式定义了一个高层接口,使得子系统更加容易使用。 外观模式的核心思想是通过引入一个外观类,将复杂子系统的内部复杂性隐藏起来,只暴露一个简单的接口给客户端。 为什么需要外观模式 外观模式要解决的核心问题是:简化复杂子系统的使用,为客户端提供一个更简单的接口。 问题场景:假设我们正在开发一个视频播放功能,需要处理音频、视频、字幕等多个子系统。 直接使用各个子系统: class VideoPlayerController { func playVideo(url: URL) { // 1. 初始化音频引擎 let audioEngine = AudioEngine() audioEngine.configure(sampleRate: 44100) audioEngine.setOutputDevice(.speaker) // 2. 初始化视频解码器 let videoDecoder = VideoDecoder() videoDecoder.setCodec(.h264) videoDecoder.configureHardwareAcceleration(true) // 3. 初始化渲染器 let renderer = VideoRenderer() renderer.setResolution(1920, 1080) renderer.setFrameRate(30) // 4. 加载字幕 let subtitleLoader = SubtitleLoader() let subtitles = subtitleLoader.load(from: url) // 5. 同步各个组件 let synchronizer = AVSynchronizer() synchronizer.link(audio: audioEngine, video: videoDecoder) // 6. 开始播放 let data = loadVideoData(url) let decodedVideo = videoDecoder.decode(data) renderer.render(decodedVideo) audioEngine.play() // 还要处理暂停、seek、音量调节... } } 这种方式有什么问题? ...

May 2, 2026

动态化 DSL 详解

动态化 DSL(Domain-Specific Language,领域特定语言)是"不改 App 主包、不走应用商店审核,只下发一段描述文件就能改变界面和逻辑"的一类技术。它在 2015~2023 年成为国内大厂客户端的必备基础设施:阿里 DinamicX / Tangram / VirtualView / LuaView、字节 Lynx、腾讯 Mars / 小程序、滴滴 Hummer、美团 MRN / Recce、京东 Taro-Native、拼多多 Lego、支付宝 Nebula / BirdNest、爱奇艺 Doric ……几乎每一家超级 App 都至少造过一套自己的动态化 DSL。 进入 2026 年 AI Coding 时代,动态化 DSL 的定位发生了根本性转变: 过去它服务于"业务迭代速度"——解决包大小限制、审核周期、多端复用问题。 现在它服务于"Agent 生成 UI"——LLM 更擅长生成 JSON / 结构化 DSL 而不是跨平台原生代码,DSL 成为 Generative UI / SDUI(Server-Driven UI)落地移动端的唯一高效载体。 本文从定义出发,讲透动态化 DSL 的历史演进、分类体系、核心渲染管线、关键原理(AST / 表达式引擎 / 布局 / 数据绑定 / 差分),再深入解剖 DinamicX、Tangram、LuaView、Lynx、Hummer、Recce 六大主流方案的工程实现,最后系统分析 AI Coding 时代的优劣势与决策矩阵。 ...

May 2, 2026

WebView离线包

背景 Hybrid 开发模式下,WebView 加载 H5 页面的体验一直是痛点。一个典型的 H5 页面加载流程涉及:初始化 WebView -> DNS 解析 -> 建立连接 -> 下载 HTML -> 解析 HTML -> 下载 CSS/JS/图片 -> 渲染页面。在弱网或首次加载场景下,白屏时间常常达到 2~5 秒,远不如 Native 体验。 离线包的核心思路是:将 H5 的静态资源(HTML、CSS、JS、图片、字体等)预先打包下发到客户端本地,WebView 加载时直接从本地读取资源,跳过网络请求环节,从而大幅缩短页面加载时间。 离线包加载 vs 在线加载 sequenceDiagram participant App participant WebView participant Local as 本地离线包 participant Server as 远程服务器 Note over App, Server: 在线加载流程 App->>WebView: loadURL WebView->>Server: DNS + TCP + TLS + HTTP请求 Server-->>WebView: HTML WebView->>Server: 请求 CSS/JS/图片 Server-->>WebView: 资源文件 WebView->>WebView: 渲染页面 Note over App, Server: 离线包加载流程 App->>WebView: loadURL(被拦截) WebView->>Local: 读取本地 HTML Local-->>WebView: HTML WebView->>Local: 读取本地 CSS/JS/图片 Local-->>WebView: 资源文件 WebView->>WebView: 渲染页面 对比项 在线加载 离线包加载 首屏时间 2~5秒(弱网更久) 0.5~1秒 网络依赖 强依赖 仅更新时需要网络 白屏问题 严重 基本消除 资源新鲜度 实时最新 有一定延迟 流量消耗 每次访问都消耗 仅增量更新消耗 整体架构 一个完整的离线包系统包含三大部分: ...

May 2, 2026

KVC底层原理

基础概念 KVC(Key-Value Coding,键值编码)是 Apple 提供的一种通过字符串 key 间接访问对象属性的机制,定义在 NSKeyValueCoding 协议中,NSObject 默认遵循该协议。 // 直接访问 person.name = @"Tom"; NSString *name = person.name; // KVC 访问 [person setValue:@"Tom" forKey:@"name"]; NSString *name = [person valueForKey:@"name"]; KVC 的核心价值在于:通过字符串动态访问属性,不需要在编译期知道属性名。这使得字典转模型、序列化/反序列化、Interface Builder 的 Runtime Attributes 等场景成为可能。 setValue:forKey: 的底层流程 当调用 [obj setValue:value forKey:@"name"] 时,Runtime 按照以下顺序查找: flowchart TD A["setValue:forKey:@'name'"] --> B{"查找 setter 方法"} B -->|"找到"| C["调用 setter"] B -->|"未找到"| D{"accessInstanceVariablesDirectly返回 YES?"} D -->|"YES"| E{"按顺序查找实例变量_name → _isName → name → isName"} D -->|"NO"| F["调用 setValue:forUndefinedKey:默认抛出 NSUndefinedKeyException"] E -->|"找到"| G["直接设置实例变量值"] E -->|"未找到"| F 第一步:查找 setter 方法 Runtime 按以下顺序查找 setter 方法: ...

May 2, 2026