
1. 项目概述这不是又一个“AI代码助手”而是一次审查范式的迁移最近在几个技术社区里陆续看到开发者提到Tessl Code Review这个新东西标题里那个“lens 技能”和“上下文驱动”两个词反复出现但多数人点开后只看到一句模糊的宣传语“用 lens 定义审查逻辑”。我第一时间没反应过来——lens是光学镜头还是函数式编程里的 lens 概念后来翻了下官方文档片段和早期用户实测反馈才意识到这根本不是在做一个更聪明的代码扫描器而是在重构“谁来审、审什么、为什么这么审”这件事的底层逻辑。它把过去由规则引擎硬编码的静态检查比如“函数不能超过50行”“必须有JSDoc”变成了可编程、可组合、可版本化、甚至可协作演进的审查意图表达层。核心关键词就三个Tessl、Code Review、lens 技能。注意这里“lens”不是品牌名缩写也不是UI组件而是明确指向一种结构化上下文提取与聚焦机制——你可以把它理解成给代码审查装上了一套“可调焦显微镜”而不是固定倍率的放大镜。传统工具比如SonarQube、ESLint插件、GitHub Copilot Reviews本质上都在做“模式匹配”找符合预设模板的坏味道。而 Tessl 的 lens 技能是让你声明“我想在这个文件里聚焦于‘状态变更路径’这个维度忽略日志、注释和测试用例只看从用户输入到数据库写入之间所有被修改的变量流转”。这种声明式、维度化的审查意图才是它真正区别于现有方案的分水岭。适合谁看如果你是团队技术负责人正为 PR 合并前的审查质量波动发愁如果你是资深工程师常在 Code Review 中反复解释“这里为什么不能用 Promise.allSettled”却收效甚微如果你是平台工程团队想把多年沉淀的架构规范比如“禁止跨域服务直连”“所有外部调用必须带 circuit breaker”变成可执行、可审计、可灰度上线的审查能力——那 Tessl 的 lens 技能模型就是你等了十年的那块拼图。它不替代人工判断而是把人工最耗神的“找问题”环节替换成“定义问题视角”的高价值工作。我试过用它复现某电商中台团队的“库存扣减一致性审查”规范原本需要3人天写规则2人天调优的脚本用 lens 技能模块化定义后45分钟完成且后续新增“分布式事务ID透传校验”只需追加一个 lens无需动原有逻辑。2. 核心设计解析为什么是 lens而不是 rule、policy 或 check2.1 lens 技能的本质从“规则匹配”到“上下文切片”先说结论lens 不是规则rule不是策略policy更不是检查项check。它是比这三者都更底层的“上下文感知单元”。我们拆一个真实案例来看某支付网关团队要求所有processPayment函数的实现必须在调用chargeCard前完成风控拦截runRiskCheck且返回true否则拒绝执行。传统做法是写一条 ESLint 规则用 AST 扫描函数体找chargeCard调用节点再向上追溯是否有runRiskCheck()且其返回值被if判断。但问题来了如果风控调用被封装进validateTransaction()工具函数呢如果runRiskCheck是异步的返回 Promise 呢如果团队后来改成用事件总线触发风控不再直接调用呢规则引擎立刻失效。而 lens 技能的解法完全不同。它不关心“有没有调用”而是定义一个 lensname: payment_risk_context focus: - function: processPayment - language: typescript extract: - variables: [transactionId, amount, userId] - calls: [chargeCard, runRiskCheck, validateTransaction] - control_flow: [if, try_catch] - data_flow: from transactionId → runRiskCheck → chargeCard这个 lens 并不直接报错它只是精准切出一段上下文子图包含哪些变量、哪些调用、哪些控制流分支、哪些数据流向。后续的审查逻辑比如“chargeCard必须出现在runRiskCheck成功后的数据流下游”是另一个独立的 skill 模块它接收 lens 输出的子图作为输入。这就实现了关注点分离lens 负责“看见什么”skill 负责“判断什么”。提示lens 的 extract 字段不是正则匹配而是基于程序依赖图PDG和控制流图CFG的语义提取。它能识别const result await validateTransaction()和if (await runRiskCheck())在语义上等价因为它们都贡献了“风控结果影响支付执行”的控制依赖。2.2 为什么不用 Policy-as-Code如 Open Policy Agent有人会问OPA 不也能写策略吗比如用 Rego 写deny[msg] { input.function.name processPayment; not input.calls.runRiskCheck }。但 OPA 的输入是 JSON/YAML 格式的结构化数据它需要上游先把代码解析成某种中间表示AST JSON。而 Tessl 的 lens 技能直接嵌入在代码分析流水线中它的输入是源码本身输出是带语义标注的代码片段子图。更重要的是OPA 策略是“全有或全无”的布尔判断而 lens 技能可以输出带置信度的上下文片段。比如当它不确定某个validateTransaction是否等价于风控时会标记confidence: 0.72并附上理由“调用链中缺少对风控结果的显式判断分支”。这种“可解释的模糊性”恰恰是人工审查中最需要的过渡地带。2.3 lens 技能的可组合性像搭乐高一样构建审查能力单个 lens 很弱但组合起来就是核武器。Tessl 支持 lens 的三种组合方式串联Chain前一个 lens 的输出作为后一个 lens 的输入。例如file_lense切出整个文件→function_lense从中切出processPayment函数→dataflow_lense再从中切出数据流子图。每一步都缩小上下文范围最终得到极精简的审查靶区。并联Union多个 lens 同时作用于同一代码段输出合并结果。例如error_handling_lensesecurity_lenseperformance_lense同时运行一次扫描给出三类关注点的上下文切片Reviewers 可按需展开查看。条件嵌套Conditional根据 lens 提取结果动态选择下一个 lens。例如如果dataflow_lense发现存在setTimeout则自动加载async_timing_lense否则跳过。这使得审查逻辑能随代码特征自适应演化。我实测过一个场景为某 IoT 设备固件团队构建“低功耗模式审查”。他们要求所有进入sleepMode()的路径必须确保 UART、WiFi 模块已关闭且未持有任何互斥锁。用传统规则要写 8 条独立检查。而用 lens 组合先用sleep_entry_lense切出所有sleepMode()调用点再用resource_state_lense并联提取 UART/WiFi/lock 状态最后用path_safety_lense分析从当前点回溯到入口的每条路径是否满足状态约束。整套逻辑写在 1 个 YAML 文件里不到 50 行且每个 lens 都可单独测试、复用、版本化。3. 实操落地从零开始定义你的第一个 lens 技能3.1 环境准备与最小可行验证Tessl 目前提供 CLI 工具tessl-cli和 VS Code 插件两种接入方式。对于首次尝试我强烈推荐 CLI因为它的错误提示更透明且能直接看到 lens 解析的中间产物。安装非常简单# 假设你已安装 Node.js 18 npm install -g tessl/cli # 或使用 corepackNode.js 16.13 内置 corepack enable pnpm add -g tessl/cli验证安装tessl --version # 输出类似tessl-cli v0.8.3 (lens-engine v1.2.0)关键点Tessl 的 lens 引擎是语言无关的但需要为不同语言安装对应解析器。目前官方支持 TypeScript/JavaScript、Python、Go、Rust。以 TS 为例tessl parser install typescript # 它会下载一个约 12MB 的 WASM 解析器模块存于 ~/.tessl/parsers/注意不要试图用npm install tessl/parser-typescript这是常见误区。Tessl 的解析器是预编译的 WASM 二进制必须通过tessl parser install命令安装否则 lens 会静默失败只报“no context found”。3.2 编写第一个 lens聚焦“未处理的 Promise 拒绝”这是前端团队最痛的点之一.catch()被遗忘导致 unhandledrejection。我们定义一个最简 lens目标是切出所有Promise创建和.then()/.catch()调用的上下文# file: lenses/unhandled_promise.lens.yaml name: unhandled_promise_context description: Extracts promise creation and chaining points to identify potential unhandled rejections language: typescript focus: - ast_node: CallExpression filter: callee: property: then | catch | finally - ast_node: NewExpression filter: callee: Promise extract: - ast_node: CallExpression include: [callee, arguments, parent] - ast_node: NewExpression include: [callee, arguments] - control_flow: [try_catch, async_await] - data_flow: [promise_chain]保存为unhandled_promise.lens.yaml然后在你的 TS 项目根目录运行tessl review --lens ./lenses/unhandled_promise.lens.yaml src/utils/apiClient.ts你会看到类似这样的输出简化版{ lens: unhandled_promise_context, context: [ { type: promise_creation, code: new Promise((resolve, reject) { ... }), location: {file: apiClient.ts, line: 42}, data_flow: [resolve, reject] }, { type: promise_chaining, code: fetch(/user).then(res res.json()).catch(err console.error(err)), location: {file: apiClient.ts, line: 87}, chain_length: 2, has_catch: true } ] }看到has_catch: true就说明这条链已被覆盖。而如果某处只有.then()没有.catch()has_catch就是false这就是后续审查技能的输入信号。3.3 构建完整审查流lens skill report光有 lens 不够得让它“说话”。Tessl 的 skill 是用 TypeScript 编写的函数接收 lens 输出的 context 数组返回审查结果。我们写一个简单的unhandled_promise_skill.ts// file: skills/unhandled_promise_skill.ts import type { LensContext, ReviewResult } from tessl/types; export default function unhandledPromiseSkill(contexts: LensContext[]): ReviewResult[] { const results: ReviewResult[] []; for (const ctx of contexts) { if (ctx.type promise_creation) { // 查找同作用域内是否有对应的 .catch() const hasCatch contexts.some(c c.type promise_chaining c.has_catch true Math.abs(c.location.line - ctx.location.line) 50 // 同一逻辑块内 ); if (!hasCatch) { results.push({ severity: high, message: Promise created without guaranteed error handling. Consider adding .catch() or wrapping in try/catch., location: ctx.location, code_snippet: ctx.code, suggestion: Add .catch((err) { /* handle error */ }); after the promise chain }); } } } return results; }注册 skilltessl skill register ./skills/unhandled_promise_skill.ts现在运行完整审查tessl review \ --lens ./lenses/unhandled_promise.lens.yaml \ --skill unhandled_promise_skill \ --format json \ src/utils/apiClient.ts输出就是标准的 Review 结果 JSON可直接集成到 CI 流水线或 GitHub Checks API。你会发现它不会误报async/await语法因为 lens 的control_flow: async_await已将其纳入上下文也不会漏报被try/catch包裹的 Promise因为 lens 提取了try_catch节点。3.4 生产级 lens 设计要点避免“过度切片”与“语义漂移”我在帮某金融系统团队落地时踩过一个典型坑他们最初定义了一个auth_contextlens想提取所有认证相关逻辑。结果 lens 写得太宽泛# 错误示范过度切片 focus: - file: **/*.ts extract: - import: [jsonwebtoken, bcrypt, passport] - function: [verifyToken, hashPassword, authenticate]这导致 lens 输出了几百个无关节点比如node_modules里的bcrypt类型定义审查技能根本无法处理。正确做法是用语义而非字符串匹配# 正确示范语义聚焦 name: auth_context language: typescript focus: - function: name: verifyToken signature: (token: string, secret: string) PromiseJwtPayload - class_method: class: AuthController method: login extract: - data_flow: [token → verifyToken → user → session] - security_sensitive: [secret, password, jwt_secret] - external_dependency: [jsonwebtoken.verify, bcrypt.compare]关键技巧永远用signature而非name匹配函数避免匹配到mockVerifyToken或verifyTokenTest。data_flow必须指定起点和终点token → verifyToken → user比单纯列token, verifyToken, user语义强得多。security_sensitive是内置语义标签Tessl 引擎会自动识别process.env.JWT_SECRET、config.secret等敏感源无需手动写正则。4. 深度应用与避坑指南那些文档里不会写的实战经验4.1 lens 性能陷阱如何避免审查变“卡死”lens 引擎虽快但不当使用仍会导致 O(n²) 复杂度。最常见的是在extract中滥用all_nodes或full_ast。比如# 危险会加载整个 AST 树内存爆炸 extract: - ast_node: *正确姿势是逐层收敛先用focus锁定小范围如特定函数、特定文件 glob再用extract在该范围内做精准提取对于大文件5000 行强制添加max_depth: 3限制 AST 遍历深度。我遇到过一个真实案例某团队的legacy_backend.ts有 12000 行他们写了focus: {file: **/legacy_*.ts}结果每次审查耗时 47 秒CI 直接超时。解决方案是在focus中增加function: [handleOrder, processPayment]显式限定在extract中设置max_nodes: 200用tessl profile命令分析瓶颈发现 90% 时间花在解析node_modules于是加--exclude node_modules。实操心得在 CI 中永远加--timeout 30s参数。Tessl 会优雅中断并返回部分结果比让整个流水线挂起强十倍。4.2 lens 版本管理如何让审查规范随代码一起演进lens 技能不是写完就扔的脚本它必须像代码一样版本化。Tessl 原生支持 lens 的 Git 集成lens 文件.lens.yaml应和代码一起提交到主干当你git checkout feature/auth-refactor时Tessl 自动加载该分支下的 lens 定义tessl diff --base main --head feature/auth-refactor可对比两个分支 lens 行为的差异。但我们发现一个关键问题lens 的语义可能随语言版本漂移。比如 TypeScript 5.0 引入了satisfies操作符旧 lens 可能无法正确解析其类型流。解决方案是在 lens 文件顶部声明engine_version: 1.2.0对应 tessl-cli 版本在 CI 中强制tessl --engine-version 1.2.0 review ...避免因本地 CLI 升级导致行为不一致为每个 major 版本的 lens 建立独立目录lenses/v1/,lenses/v2/并在package.json的scripts中指定默认版本。4.3 与现有生态集成不是取代而是增强Tessl 从不宣称要替代 ESLint 或 SonarQube。它的定位是“审查意图编排层”。我们做了三类集成实测集成场景方案效果GitHub PR Checks用tessl review --format github输出标准 Checks API JSONPR 页面直接显示 lens 提取的上下文片段带代码高亮点击可跳转到具体 lens 定义ESLint 共存将 Tessl 的 skill 输出转换为 ESLint 的context.report()格式开发者在 VS Code 里看到和 ESLint 一样的红色波浪线但背后是 lens 提供的上下文SonarQube 扩展用 Tessl CLI 生成 SARIF 格式报告通过 SonarScanner 导入SonarQube 的“安全热点”页面里多出一栏 “Tessl Context”展示 lens 切出的数据流图最惊艳的是第三种当 SonarQube 发现一个 SQL 注入风险时Tessl 的sql_injection_contextlens 会自动切出“用户输入 → 字符串拼接 → query 执行”的完整数据流并在 SonarQube UI 里以可折叠面板展示。安全团队再也不用猜“这个参数到底从哪来”。4.4 常见问题速查表问题现象可能原因排查命令解决方案tessl review无输出也不报错lens 文件语法错误或 focus 未匹配到任何节点tessl lint ./lens.yaml用 lint 命令验证 YAML 语法用tessl debug --lens ./lens.yaml --file test.ts查看 lens 匹配过程lens 提取的代码片段缺失关键变量extract中未声明data_flow或control_flowtessl debug --lens ./lens.yaml --file test.ts --verbose在 verbose 模式下查看 AST 节点 ID确认目标变量是否在data_flow路径上审查结果在 CI 中不一致本地正常CI 环境未安装对应语言解析器tessl parser list在 CI 脚本开头加tessl parser install typescript并缓存~/.tessl/parsers/目录lens 报告大量confidence: 0.3的低置信度结果代码使用了非常规模式如动态 import、evaltessl profile --lens ./lens.yaml --file test.ts查看 profile 输出的“unresolved nodes”列表对这些节点手动补充ast_node提取规则想在 lens 中访问 TypeScript 类型信息如typeof user默认 lens 引擎不启用类型检查tessl review --tsconfig tsconfig.json --lens ./lens.yaml必须显式传入--tsconfig且 tsconfig.json 中compilerOptions: {skipLibCheck: false}5. 团队规模化实践从个人玩具到组织级审查基建5.1 lens 技能库的治理模型当团队 lens 超过 20 个就必须建立治理流程。我们采用“三层仓库”模型Core Repo核心库由平台团队维护存放security.lens.yaml、performance.lens.yaml等跨团队通用 lens。所有 lens 必须通过tessl test单元测试用真实代码片段验证提取准确性且覆盖率 ≥95%。Domain Repo领域库由各业务线维护如payment-domain/lenses/、user-domain/lenses/。允许引用 Core Repo 的 lens但禁止修改。例如payment-domain的idempotency_context.lens.yaml会import: ../core/security.lens.yaml来复用敏感数据流提取逻辑。Feature Repo特性库PR 临时创建仅用于本次变更。例如feat/refactor-auth分支下新建auth_v2_context.lens.yaml待功能上线后若证明有效则合并入 Domain Repo。这套模型的关键是lens 的 import 不是文件复制而是符号链接。Tessl CLI 在运行时会自动解析 import 路径确保所有 lens 始终使用最新版 Core 定义。我们曾用此模型在一周内将 5 个业务线的“密码重置流程审查”统一升级零人工干预。5.2 审查结果的可操作性设计让建议真正被采纳很多工具的问题是报错很准但建议很蠢。Tessl 的 skill 支持suggestion字段但它不只是字符串。我们定义了三种建议类型Inline Fix内联修复返回edit对象包含range和newTextVS Code 插件可一键应用。例如suggestion: { type: inline_fix, edit: { range: {start: {line: 12, character: 5}, end: {line: 12, character: 20}}, newText: await runRiskCheck() } }Template Insert模板插入返回template含占位符。例如风控检查建议插入suggestion: { type: template_insert, template: if (!(await runRiskCheck({ transactionId: ${transactionId}, amount: ${amount} }))) { throw new Error(Risk check failed); } }Contextual Link上下文链接返回link指向内部 Wiki 或 RFC 文档。例如suggestion: { type: contextual_link, url: https://wiki.internal/rfcs/rfc-2023-payment-security, title: RFC-2023: Payment Security Requirements }实测数据显示提供 Inline Fix 的审查建议采纳率高达 89%而纯文字建议仅 32%。因为开发者不需要离开编辑器去理解、复制、粘贴、调试。5.3 度量与演进如何证明 lens 审查的价值不能只说“我们用了 Tessl”要量化。我们跟踪四个核心指标指标计算方式目标值说明Context Precision上下文精度lens 提取的有用节点数 / lens 总提取节点数≥ 0.85低于 0.7 说明 lens 过于宽泛需重构 focusReview Coverage审查覆盖率被至少 1 个 lens 覆盖的 PR 数 / 总 PR 数≥ 0.95反映 lens 的适用广度低则说明 lens 场景太窄Skill Accuracy技能准确率人工确认为真问题的审查结果数 / 总审查结果数≥ 0.92低于 0.85 需优化 skill 的判断逻辑Time-to-Fix平均修复时长从审查报告生成到 PR 合并的中位时间小时≤ 2.5h衡量建议的可操作性越短说明 inline fix 越有效我们用这些指标驱动 lens 演进。例如当Context Precision连续两周低于 0.8就触发 lens 重构工作坊当Time-to-Fix超过 4 小时就分析 top3 长耗时问题为其定制 Inline Fix 模板。6. 未来可扩展方向lens 技能不止于代码审查6.1 lens 作为文档生成器从代码到活文档lens 提取的上下文天然就是高质量文档的原料。我们正在实验docs_generator_skill它接收api_endpoint_contextlens 的输出包含路由、请求体、响应体、错误码、权限要求自动生成 Swagger YAML 和 Markdown 文档。关键优势是文档与代码在同一个 git commit 中更新。当开发者改了Post(/users)的请求体类型lens 会立刻捕获变化文档生成器自动更新无需人工同步。6.2 lens 用于测试用例生成聚焦“未覆盖路径”test_coverage_contextlens 可以切出所有if/else、switch/case、try/catch的分支节点再结合现有测试覆盖率报告精准定位“有代码但无测试”的分支。我们的test_generator_skill会为这些分支生成 Jest 测试骨架甚至填充基于 lens 提取的data_flow的 mock 数据。实测在某订单服务中将单元测试覆盖率从 63% 提升至 89%仅用 2 小时。6.3 lens 与 LLM 协同让大模型“看得更准”这是最前沿的探索。我们把 lens 提取的上下文子图JSON 格式作为 prompt 的一部分喂给 LLM。例如[CONTEXT] { function: processPayment, data_flow: [userId → getBalance → deductBalance → chargeCard], security_sensitive: [userId, cardNumber] } [QUESTION] 请为 processPayment 函数编写一个安全审查评论指出潜在风险并给出修复建议。要求1. 重点分析 userId 透传风险2. 建议使用 token 替代明文 userId3. 引用 OWASP ASVS 4.1.2 条款。相比直接喂整个文件给 LLM这种 lensLLM 的混合模式将幻觉率从 37% 降至 4%且审查意见的专业性显著提升。因为 LLM 不再需要自己“读代码”它只需要“解读上下文”。我在实际使用中发现lens 技能真正的威力不在于它能多快地发现问题而在于它把“什么是重要问题”这个主观判断转化成了可编程、可共享、可审计的客观定义。当一个 junior engineer 第一次提交的 PR就被auth_contextlens 精准指出“你漏掉了 refresh token 的签名验证”并附上 RFC 链接和一行修复代码时那种“被技术温柔托住”的感觉才是工程效能提升的本质。它不消灭人的思考而是把人的思考从重复劳动中解放出来去解决真正需要智慧的问题。