news 2026/5/9 7:29:51

AI代码质量守护:eslint-plugin-ai-guard 插件实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI代码质量守护:eslint-plugin-ai-guard 插件实战指南

1. 项目概述:为什么我们需要一个专为AI代码“体检”的ESLint插件?

如果你和我一样,在日常开发中已经离不开GitHub Copilot、Cursor或者Claude Code这类AI编程助手,那你肯定也经历过那种“哭笑不得”的时刻:AI生成的代码看起来逻辑清晰、语法正确,TypeScript检查也全绿,但一运行就出幺蛾子。最常见的就是异步操作没await,错误被catch了但什么都没处理,或者直接把API密钥写死在代码里。这些Bug往往很隐蔽,在代码审查时容易被忽略,直到上线后才暴露,让人头疼不已。

这就是eslint-plugin-ai-guard诞生的背景。它不是一个通用的代码质量检查工具,而是一个精准的“AI代码缺陷扫描仪”。根据CodeRabbit 2025年的报告,AI生成的代码比人类代码存在1.7倍更多的问题2.74倍更多的安全漏洞。现有的ESLint规则(如eslint-plugin-promise@typescript-eslint系列)主要针对人类开发者常见的错误模式,而ai-guard则专门捕捉那些AI工具最常犯、且最具迷惑性的“反模式”。

简单来说,它填补了现有工具链的一个关键空白:当AI成为你的“结对编程”伙伴时,你需要一个懂AI“思维习惯”的代码审查员。这个插件适合所有正在或计划大规模使用AI辅助编码的团队和个人开发者,无论是前端、后端还是全栈项目,只要你的代码库里有JavaScript或TypeScript,它就能帮你守住质量底线,把AI带来的效率提升真正转化为稳定可靠的生产力。

2. 核心设计哲学:从“可用”到“敢用”的渐进式实践

很多代码质量工具一开始雄心勃勃,试图用最严格的规则一次性改造整个代码库,结果往往因为告警太多、改造成本太高而被团队弃用。ai-guard的设计者显然深谙此道,它的核心理念不是“颠覆”,而是“平滑嵌入”。

2.1 三级预设策略:匹配团队的不同成熟度

ai-guard提供了三个开箱即用的预设(recommendedstrictsecurity),这不仅仅是规则严格程度的区别,更代表了三种不同的落地策略。

  • recommended(推荐预设):这是默认的、也是作者最推荐的起点。它的设计目标是“低噪音、高价值”。只将那些确信无疑会导致Bug或安全漏洞的规则设置为error级别(例如:空catch块、硬编码密钥)。对于一些需要结合上下文判断的规则(如“是否使用了过于宽泛的异常捕获”),则设置为warn。这样,团队在首次引入时,不会面对海量的错误提示,可以快速修复最关键的问题,建立对工具的信心。
  • strict(严格预设):当团队已经适应了ai-guard,并且希望追求更高的代码一致性时,可以切换到严格模式。此模式下,所有规则都将作为error启用,包括那些关于代码风格和潜在不良实践(如“catch块仅记录日志并重新抛出”、“在HTTP处理程序中使用console.log”)的规则。这适合代码规范成熟、追求极致质量的团队。
  • security(安全预设):专为安全审计场景设计。它只启用与安全直接相关的规则,并将最关键的几项(如SQL注入风险、不安全的反序列化)设为error。这对于在现有大型代码库中快速进行安全漏洞扫描特别有用,可以让你聚焦在最危险的问题上。

这种分级策略的精妙之处在于,它承认了“完美”是“良好”的敌人。先让工具用起来,解决最痛的问题,再逐步深化,这是任何新工具在团队中成功推广的关键。

2.2 “安全自动修复”的务实主义

自动修复(Autofix)是提升开发者体验的利器,但用不好就是灾难。ai-guard在实现自动修复功能时,体现出了高度的务实和谨慎。

它并非对所有规则都提供修复,而是精选了几条模式固定、修复方案明确、几乎不会出错的规则。例如:

  • no-empty-catch: 自动在空catch块中插入{ /* TODO: handle error */ }注释。这比直接删除catch或抛出新错误要安全得多,因为它明确标记了此处需要后续处理,避免了破坏现有的错误传播逻辑。
  • no-hardcoded-secret: 尝试将类似const apiKey = 'sk-xxx'的代码替换为const apiKey = process.env.API_KEY。这是一个强烈的安全提示,引导开发者使用环境变量。
  • no-floating-promise: 在确认为“故意不等待”的Promise前添加void操作符(如void fetchData())。void明确表达了开发者的意图:“我知道这个Promise没被等待,我是故意的”,这既消除了lint错误,又提高了代码的可读性。

这些修复被设计为“安全”的,意味着它们倾向于做最小、最保守的变更,而不是尝试进行复杂的代码重构。对于no-await-in-loop这样的规则,它只会在循环体内部操作完全独立、明显可以并行化的简单情况下,才尝试将其重构为Promise.all。如果逻辑复杂,它宁愿报错,让开发者手动处理。这种“宁可错过,不可改错”的原则,是生产级工具应有的责任感。

3. 核心规则深度解析与避坑指南

ai-guard的规则库是其价值核心。理解每条规则背后的“为什么”,能帮助我们在实际编码和审查中做出更明智的判断。下面我们分类深入探讨。

3.1 异步稳定性:AI的“并发幻想症”

异步编程是AI助手最容易“翻车”的领域之一。AI模型基于统计概率生成代码,它很容易写出“看起来对”的异步模式,却忽略了JavaScript事件循环的实际执行顺序。

  • no-async-array-callback(禁止异步数组回调)

    • 问题本质Array.prototype.mapfilterforEach等方法,会同步地遍历数组,并对每个元素同步地调用回调函数。如果回调是async函数,那么每次调用都会立即返回一个Promise对象,而不会等待这个Promise解决。map(async (x) => await doSomething(x))的结果是一个Promise数组,而不是你期望的结果值数组。
    • AI为何常犯:AI在理解“集合操作”与“异步操作”的组合时,容易产生“魔法”联想,认为map会自动处理异步。它生成的代码在静态类型检查(TypeScript)下是合法的,因为类型可能是Promise<T>[],但在运行时逻辑完全错误。
    • 正确做法
      // ❌ AI可能生成(错误) const userPromises = userIds.map(async (id) => await fetchUser(id)); // `userPromises` 是 [Promise, Promise, Promise] // ✅ 正确做法:使用 Promise.all 等待所有异步操作完成 const users = await Promise.all(userIds.map((id) => fetchUser(id))); // `users` 是 [User, User, User]
    • 实操心得:这条规则是“必开项”。它捕获的是一类非常隐蔽的Bug,症状往往是“数据莫名其妙是空的”或“后续操作报未定义错误”。在Code Review时,看到array.map里出现async关键字就要立刻警惕。
  • no-floating-promise(禁止浮动的Promise)

    • 问题本质:调用了一个返回Promise的函数,但没有用await.then/.catchPromise.all等方式处理它。这个Promise会独立运行,但如果它被拒绝(rejected),且没有被捕获,这个错误可能会在某个时刻导致进程崩溃(在Node.js中)。
    • AI为何常犯:AI在生成调用第三方API、数据库查询或文件操作的代码时,有时会忘记这些操作是异步的。它可能写出一行sendEmail(user),却漏掉了前面的await
    • 正确做法
      // ❌ 浮动Promise,错误可能被静默吞没 updateUserStatus(userId, 'inactive'); // ✅ 等待它完成 await updateUserStatus(userId, 'inactive'); // 或明确表示“触发后不管” void updateUserStatus(userId, 'inactive'); // `ai-guard`的自动修复方案 // 或处理可能的错误 updateUserStatus(userId, 'inactive').catch(logError);
    • 注意事项void操作符是一个很好的模式,它明确告诉Linter和后来的开发者:“这个Promise我不关心其结果,故意不等待”。这比用// eslint-disable-next-line注释要好,因为意图更清晰。
  • no-await-in-loop(禁止在循环中使用await)

    • 问题本质:在forwhile等循环中,如果每次迭代的await操作是相互独立的,那么顺序执行会导致不必要的性能损失。总耗时是所有单个操作耗时的总和。
    • AI为何常犯:AI生成的代码逻辑常常是线性的、一步一步的。当它需要处理一个列表时,很自然地会写出for const item of items) { await process(item); }这样的模式,而没有考虑并发优化。
    • 正确做法
      // ❌ 顺序执行,慢 for (const id of userIds) { const user = await fetchUser(id); // 每个都等完才下一个 results.push(user); } // ✅ 并行执行,快(当操作独立时) const userPromises = userIds.map(id => fetchUser(id)); const results = await Promise.all(userPromises);
    • 重要例外:这条规则是“智能”的。如果循环体中的后一次操作依赖于前一次的结果(例如,分页查询、有状态的操作),那么使用await in loop就是正确的。ai-guard的规则实现会尝试分析这种依赖性,减少误报。在确实需要顺序执行的地方,可以使用// eslint-disable-next-line ai-guard/no-await-in-loop进行注释说明。

3.2 错误处理:AI的“乐观主义陷阱”

AI倾向于生成“happy path”的代码,对于错误情况的处理往往流于形式,甚至直接忽略。

  • no-empty-catch(禁止空catch块)

    • 问题本质:空的catch块会静默吞掉所有错误,使得程序在发生异常时没有任何日志、没有任何恢复动作,行为不可预测,是调试的噩梦。
    • AI为何常犯:当AI生成一个try...catch结构时,它的首要目标是让代码“不报错”,因此catch块经常被生成出来,但里面却没有具体的处理逻辑。有时它甚至会生成catch (e) {}这种最糟糕的形式。
    • 正确做法:至少应该记录错误。ai-guard的自动修复会插入一个TODO注释,这是一个很好的起点。
      // ❌ 错误被无声无息地吃掉 try { riskyOperation(); } catch {} // ✅ 至少记录日志 try { await riskyOperation(); } catch (error) { // ai-guard 自动修复会生成这个 console.error('Failed to perform operation:', error); // 或者根据上下文:重试、返回默认值、抛出自定义错误等 // throw new ApplicationError('Operation failed', { cause: error }); }
  • no-broad-exception(禁止宽泛的异常捕获)

    • 问题本质:使用catch (e: any)catch (e: unknown)但不进行类型收窄(type narrowing),会丢失错误的类型信息,让后续的错误处理变得困难且不安全(比如你可能会误以为e一定有message属性)。
    • AI为何常犯:TypeScript下,catch块的默认参数类型是any。AI为了快速通过类型检查,最省事的办法就是直接用any。它很少会主动去判断错误的具体类型(是Error?是AxiosError?还是自定义错误?)。
    • 正确做法:始终将catch参数视为unknown,并在使用前进行检查。
      // ❌ 丢失类型信息 try { ... } catch (e: any) { console.log(e.message); // 如果e不是Error,这里可能runtime error } // ✅ 安全的类型处理 try { ... } catch (e: unknown) { if (e instanceof Error) { console.log(e.message); } else if (typeof e === 'string') { console.log(e); } else { console.log('An unknown error occurred', e); } // 或者使用类型断言工具库 }

3.3 安全防护:AI的“信任危机”

AI没有安全常识,它会根据训练数据中常见的模式生成代码,而这其中可能包含大量不安全的历史代码。

  • no-hardcoded-secret(禁止硬编码密钥)

    • 问题本质:将API密钥、数据库密码、加密盐值等直接写在源代码中,会随代码库一起被提交到版本控制系统(如Git),造成严重的安全泄露风险。
    • AI为何常犯:在教程、示例代码和旧的Stack Overflow回答中,硬编码密钥非常普遍。AI从这些数据中学到的模式就是“const apiKey = 'sk-...'”。它无法理解这在实际项目中的危险性。
    • 正确做法:一律使用环境变量或安全的密钥管理服务。
      // ❌ 直接暴露在代码中 const OPENAI_API_KEY = 'sk-abc123...'; // ✅ 从环境变量读取 const OPENAI_API_KEY = process.env.OPENAI_API_KEY; if (!OPENAI_API_KEY) { throw new Error('OPENAI_API_KEY is not configured'); }
    • 注意事项ai-guard的自动修复会尝试替换为process.env.*,但这只是一个引导。你还需要确保在项目根目录有.env文件(并加入.gitignore),并在应用启动时加载(例如使用dotenv包)。
  • no-sql-string-concat(禁止SQL字符串拼接)

    • 问题本质:使用字符串模板或+运算符将用户输入直接拼接到SQL查询语句中,是经典的SQL注入漏洞来源。
    • AI为何常犯:简单的字符串拼接是AI最容易生成的查询方式。尽管它也可能生成使用参数化查询的代码,但在复杂的动态查询场景下,它更容易退回到拼接的老路。
    • 正确做法:使用参数化查询(Prepared Statements)或成熟的查询构建器(Query Builder)。
      // ❌ 高危!SQL注入 const query = `SELECT * FROM users WHERE id = ${req.params.id}`; // ✅ 使用参数化查询(以pg库为例) const result = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]); // ✅ 使用查询构建器(以Knex为例) const users = await knex('users').where('id', req.params.id);
    • 高级特性:这条规则是“上下文感知”的。它能识别主流ORM/查询构建器(如Knex、Prisma、Sequelize、TypeORM等)的用法,对于knex('users').where('id', id)这样的代码不会误报。它只对原始的字符串拼接或无法识别的数据库调用发出警告。这大大减少了在现代化项目中的误报率。

4. 无缝集成实战:从命令行到IDE的全链路防护

ai-guard的强大不仅在于规则,更在于它提供了多种低摩擦的集成方式,让你在开发流程的各个阶段都能拦截问题。

4.1 零配置CLI:快速体验与一次性检查

对于尚未在项目中配置ESLint,或者只是想快速扫描一下现有代码库的用户,ai-guard的CLI工具是完美的入口。

# 最基本的使用,使用recommended预设扫描当前目录 npx ai-guard run # 使用严格模式进行深度扫描 npx ai-guard run --strict # 只关注安全漏洞 npx ai-guard run --security # 初始化ESLint配置,将ai-guard集成到你的项目中 npx ai-guard init

npx ai-guard run命令背后,它会临时创建一个ESLint配置,运行检查,然后输出结果。这对于在CI/CD流水线中快速添加一个AI代码质量检查步骤非常有用。npx ai-guard init命令则更近一步,它会根据你的项目情况(是eslint.config.js还是.eslintrc),自动生成正确的配置文件,将ai-guard作为插件引入。

4.2 与现有ESLint配置集成

如果你的项目已经有一套成熟的ESLint配置(无论是传统的.eslintrc.*还是新的Flat Configeslint.config.js),集成ai-guard也非常简单。

Flat Config (ESLint 9+) 示例:

// eslint.config.js import aiGuard from 'eslint-plugin-ai-guard'; import tseslint from 'typescript-eslint'; export default tseslint.config( // 你的其他配置... { plugins: { 'ai-guard': aiGuard, }, rules: { // 混合使用:启用ai-guard的recommended规则,并覆盖其中一条 ...aiGuard.configs.recommended.rules, 'ai-guard/no-broad-exception': 'error', // 将warn提升为error }, }, );

传统配置示例:

{ "plugins": ["ai-guard"], "extends": ["plugin:ai-guard/recommended"], "rules": { "ai-guard/no-console-in-handler": "warn" // 自定义某条规则 } }

实操心得:建议先从extends: ["plugin:ai-guard/recommended"]开始。观察一段时间,看看哪些规则对你的项目模式产生较多误报或警告,再通过rules字段进行精细调整。不要一开始就追求“零警告”,那可能会让你关掉很多有价值的规则。

4.3 终极防御:为AI助手本身注入规则(init-context

这是ai-guard最具有前瞻性的功能。与其等AI生成坏代码后再用linter去检查,不如直接告诉AI:“请你不要生成这类代码”。npx ai-guard init-context命令就是这个思想的实现。

运行该命令后,它会交互式地询问你使用哪些AI编程助手(Claude Code、Cursor、GitHub Copilot),然后为它们生成对应的“指令文件”。

  • CLAUDE.md:放在项目根目录,Claude Code会自动读取并遵循其中的指令。
  • .cursorrules:Cursor IDE的规则文件。
  • .github/copilot-instructions.md:GitHub Copilot的全局或仓库级指令。

这些文件里包含了针对那17种最常见AI反模式的、用自然语言编写的预防性指令。例如,它会告诉Copilot:“当用户要求编写异步操作时,避免使用array.map配合async函数,除非意图是生成Promise数组。优先建议使用Promise.all。” 或者“永远不要在代码中硬编码密钥、密码或令牌,而是使用process.env.VAR_NAME的格式,并添加必要的环境变量检查。”

效果:当你下次在Cursor里用Cmd+K生成代码,或者Copilot给你提示时,它从一开始就会避免那些有问题的模式。这相当于将质量关卡左移到了“代码生成”这一刻,极大地减少了后续修改的成本。

注意:这个功能的效果取决于AI助手对指令文件的遵循程度。根据我的实测,Claude Code和Cursor对此类项目级指令的响应非常好,Copilot则有一定波动。但无论如何,这都是一项值得配置的、成本极低的预防措施。记得在升级ai-guard后,使用npx ai-guard init-context --force来重新生成指令文件,以获取最新的规则描述。

5. 在真实工作流中落地:策略、问题排查与团队协作

引入一个新工具总会遇到阻力。如何让ai-guard平滑地融入团队,并持续发挥价值,需要一些策略。

5.1 渐进式落地策略

  1. 个人试用期:建议团队的技术负责人或感兴趣的同学先在个人分支或本地试用npx ai-guard run,感受它捕捉到的问题类型。修复几个典型的案例,体会其价值。
  2. 团队演示:在团队周会上,展示几个由AI生成、被ai-guard捕获的“经典Bug案例”。用实际代码演示这些Bug如何静默地导致功能失效或安全风险。这比单纯介绍工具更有说服力。
  3. CI集成(仅报告):在团队的CI流水线(如GitHub Actions、GitLab CI)中,添加一个ai-guard检查步骤。初期可以将其设置为非阻塞(不影响合并),只生成报告。让团队成员习惯在MR中看到ai-guard的反馈。
  4. 项目配置集成:在团队达成共识后,通过npx ai-guard init将插件正式加入项目的ESLint配置。强烈建议从recommended预设开始,并将规则违反设置为warn而非error,避免在初期引起大量红色错误阻碍开发。
  5. 提升为阻塞项:当团队已经适应,并且大部分历史问题已被清理后,可以在CI中将ai-guard的检查设为阻塞项(即检查不通过无法合并)。同时,可以考虑将部分高价值规则从warn升级为error
  6. 启用AI指令:推动团队成员在本地运行npx ai-guard init-context,为各自的AI助手配置预防性规则。这能从源头上减少问题。

5.2 常见问题与排查技巧

即使规则设计得再精妙,在实际复杂的代码库中,误报和漏报也是难免的。如何处理这些情况,决定了工具能否被长期使用。

  • 问题:规则报告了误报,但我认为代码是正确的。

    • 排查:首先,仔细阅读规则的错误信息。ai-guard的错误信息通常很详细,会解释为什么认为这是问题。例如,no-sql-string-concat可能会误报一个你自己写的、安全的SQL模板函数。
    • 解决:你有几个选择:
      1. 代码重构:也许有更清晰、更安全的写法可以避免触发规则。这是首选方案。
      2. 行内禁用:如果确认代码安全且无法改写,可以在该行使用ESLint禁用注释。
        // eslint-disable-next-line ai-guard/no-sql-string-concat const query = mySafeSqlTemplate`SELECT * FROM table WHERE id = ${id}`;
      3. 规则降级/关闭:如果某条规则在你们的代码库中误报率极高,可以在项目配置中将其从error降为warn,或直接off。但这应该是最后的手段。
  • 问题:ai-guard没有报告我发现的某个AI生成的典型错误。

    • 排查:确认你使用的预设是否包含了对应的规则。例如,no-catch-log-rethrowrecommended预设中是关闭的(off)。
    • 解决
      1. 切换到strict预设看看是否会报错。
      2. 查阅 规则列表 ,看是否有其他规则可能覆盖这种情况。
      3. 如果确认是一个新的、常见的AI反模式,而ai-guard尚未覆盖,这正是开源项目欢迎的贡献!你可以去项目的GitHub仓库,使用 Rule Request模板 提交新规则建议。描述清楚问题模式、AI为何容易犯、以及正确的代码示例。
  • 问题:自动修复(--fix)后代码无法运行或行为改变。

    • 排查ai-guard的自动修复被设计为“安全”的,但不排除在极端复杂的情况下产生意外。任何自动修复后的代码都必须经过审查和测试
    • 解决
      1. 不要盲目地一次性对整个项目运行eslint --fix。可以针对单个文件或目录进行。
      2. 仔细查看修复前后的diff,确认逻辑是否一致。特别是no-await-in-loop的修复,从循环改为Promise.all,要确保所有迭代确实是独立的。
      3. 运行相关的单元测试和集成测试。

5.3 与团队代码规范结合

ai-guard不应该取代你团队现有的代码规范和ESLint配置(如eslint-config-airbnb,@typescript-eslint/recommended等),而应该作为它们的有力补充

一个健康的配置可能是这样的:

  1. 基础规范eslint:recommended+@typescript-eslint/recommended+prettier(代码格式化)。
  2. 团队规范eslint-config-airbnb-base或你们自定义的规则集。
  3. AI专项防护plugin:ai-guard/recommended
  4. 项目自定义:在rules中覆盖或添加项目特定的规则。

这样的分层结构,确保了代码在符合通用最佳实践和团队约定的同时,还额外拥有一层针对AI生成代码缺陷的专门防护。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/9 7:25:37

多GPU大模型训练中的流水线并行技术解析

1. 多GPU大模型训练的核心挑战当模型参数量突破十亿级别时&#xff0c;单张GPU的显存容量和计算能力往往成为瓶颈。以GPT-3为例&#xff0c;其1750亿参数的全精度存储就需要约700GB显存&#xff0c;远超当前任何消费级显卡的容量。此时必须将模型拆分到多个设备上进行分布式训练…

作者头像 李华
网站建设 2026/5/9 7:25:30

腾讯云开源OpenAI、Manus同款Agent底座

4月21日&#xff0c;腾讯云宣布正式开源 Cube Sandbox。一套面向 AI Agent 的执行环境底座&#xff0c;也是业内首个兼顾硬件级隔离与亚百毫秒启动的开源沙箱服务。&#x1f31f;项目主页&#xff1a;https://github.com/TencentCloud/CubeSandbox在当前主流的 Agent 架构中&am…

作者头像 李华
网站建设 2026/5/9 7:19:30

DownKyi视频下载解决方案:从新手到专家的完整工作流

DownKyi视频下载解决方案&#xff1a;从新手到专家的完整工作流 【免费下载链接】downkyi 哔哩下载姬downkyi&#xff0c;哔哩哔哩网站视频下载工具&#xff0c;支持批量下载&#xff0c;支持8K、HDR、杜比视界&#xff0c;提供工具箱&#xff08;音视频提取、去水印等&#xf…

作者头像 李华
网站建设 2026/5/9 7:15:56

AArch64架构中的Checked Pointer Arithmetic机制解析与应用

1. AArch64架构中的Checked Pointer Arithmetic机制解析在ARMv8-A架构的安全扩展中&#xff0c;Checked Pointer Arithmetic&#xff08;CPA&#xff09;是一套用于增强内存安全性的重要机制。这个特性最初在ARMv8.5-A中引入&#xff0c;并在后续架构版本中不断强化。CPA的核心…

作者头像 李华
网站建设 2026/5/9 7:11:41

Python Monkey Patching技术详解与应用实践

1. 什么是Monkey Patching&#xff1f;Monkey patching&#xff08;猴子补丁&#xff09;是一种在运行时动态修改或扩展代码行为的技术&#xff0c;它允许开发者在不修改原始源代码的情况下&#xff0c;临时或永久地改变类、模块或对象的行为。这个术语源自于"guerilla pa…

作者头像 李华
网站建设 2026/5/9 7:09:53

Qianfan-OCR参数详解:max_num=12切块数对显存/速度/精度的平衡策略

Qianfan-OCR参数详解&#xff1a;max_num12切块数对显存/速度/精度的平衡策略 1. 工具概述 Qianfan-OCR是基于百度千帆InternVL架构开发的单卡GPU专属文档解析工具。它通过创新的动态切块技术&#xff0c;实现了对高清文档、表格、公式等复杂内容的精准解析。与传统OCR工具相…

作者头像 李华