news 2026/9/5 16:39:48

Redux 基础教程实战:用中间件与 Redux Thunk 实现异步逻辑和数据获取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redux 基础教程实战:用中间件与 Redux Thunk 实现异步逻辑和数据获取

Redux 基础教程实战:用中间件与 Redux Thunk 实现异步逻辑和数据获取

【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux

本篇内容基于 Redux 官方基金教程第 6 篇(part-6-async-logic.md),讲解当应用数据不再只存在于客户端、而需要通过与服务器进行 HTTP 请求来读写时,Redux 是如何通过**中间件(middleware)**与Thunk 函数来承载异步逻辑的:Redux 数据流在引入异步后会多出哪些环节、为什么 reducer 中不能写副作用、applyMiddleware在源码层面如何把多个中间件组装成一条 dispatch 管线,以及如何用 thunk 函数从服务端拉取待办列表并保存新待办。读完本篇,你将能够配置带 thunk 中间件的 store、编写"接收dispatchgetState的异步函数"、在组件中 dispatch thunk 完成真实的数据获取流程,并在源码与测试层面验证这套机制的每个环节。

前置知识与你将学到的内容

本教程第 6 篇建立在 第 5 篇:UI 和 React 的基础上:你已经会用react-redux让组件与 Redux store 交互——调用useSelector读取状态、调用useDispatch拿到dispatch函数、并用<Provider>组件让 hooks 访问到 store。

本篇学习目标:

  • Redux 数据流在处理异步数据时是如何工作的;
  • 如何用 Redux 中间件编写异步逻辑;
  • 处理异步请求状态(request state)的模式。

前置要求:

  • 熟悉使用 HTTP 请求从服务器获取和更新数据;
  • 理解 JS 中的异步逻辑,包括 Promise。

值得先了解的一点:Redux Toolkit 提供了专门的数据获取与缓存方案 RTK Query,它可以消除为数据获取编写任何 thunk 或 reducer 的需要。Redux 官方教程将 RTK Query 作为数据获取的默认推荐方案,而 RTK Query 正是建立在本篇讲解的同一套模式之上,参见 RTK Query 概述 与 Redux Essentials, Part 7: RTK Query Basics。本篇讲的是这些底层机制本身。

示例 REST API 与 HTTP 客户端

为了让示例项目既隔离又贴近现实,教程配套的示例应用(redux-fundamentals-example-app)在初始搭建时就内置了一个内存版的假 REST API(使用 Mirage.js 这个 mock API 工具配置)。该 API 以/fakeApi作为所有端点的基础 URL,并对/fakeApi/todos支持常见的GET/POST/PUT/DELETEHTTP 方法,定义在示例应用的src/api/server.js中。

项目还包含一个小型 HTTP API 客户端对象,暴露client.get()client.post()方法,用法类似 axios 等常见 HTTP 库,定义在示例应用的src/api/client.js中。本节的 HTTP 调用都通过这个client对象发往内存中的假 REST API。

Redux 中间件与副作用

单看一个 Redux store,它对异步逻辑一无所知:它只会同步地 dispatch 动作、通过调用根 reducer 函数更新状态,并通知 UI 发生了变化。任何异步性都必须发生在 store 之外。

在前面的教程中我们说过,Redux reducer 绝不能包含"副作用"。"副作用"指的是任何能够被函数返回值之外的外部世界观察到的状态或行为改变。常见的副作用包括:

  • 向控制台打印日志;
  • 保存文件;
  • 设置异步定时器;
  • 发起 HTTP 请求;
  • 修改函数外部存在的某个状态,或直接改变(mutate)函数参数;
  • 生成随机数或唯一随机 ID(如Math.random()Date.now())。

然而,任何真实应用都总要在某个地方做这些事情。既然 reducer 里不能放副作用,那副作用应该放在哪里

Redux 中间件正是为了让你可以编写带副作用的逻辑而设计的。

正如 第 4 篇 中所述,Redux 中间件在"看到"一个被 dispatch 的动作时可以做任何事:打印日志、修改动作、延迟动作、发起异步调用,等等。而且,由于中间件构成了包裹在真实store.dispatch函数外的一整条管线,这意味着你实际上可以向dispatch传递不是一个纯 action 对象的值——只要某个中间件拦截了这个值、并且不让它一路到达 reducer 就行。

中间件还能访问dispatchgetState。也就是说,你可以在中间件里编写异步逻辑,并且依然能够通过 dispatch 动作与 Redux store 保持交互。

Redux 作者在 StackOverflow 上多次解释过"为什么异步流需要中间件",其核心论点与本节一致:状态更新必须同步、可预测,而副作用必须被移出 reducer 到 dispatch 管线上。

用中间件启用异步逻辑

下面看两个例子,展示中间件如何让我们编写能与 Redux store 交互的异步逻辑。

一种可能是写一个查找特定 action 类型的中间件,在"看到"这些动作时运行异步逻辑:

import { client } from '../api/client' const delayedActionMiddleware = storeAPI => next => action => { if (action.type === 'todos/todoAdded') { setTimeout(() => { // 延迟这个 action 一秒 next(action) }, 1000) return } return next(action) } const fetchTodosMiddleware = storeAPI => next => action => { if (action.type === 'todos/fetchTodos') { // 发起 API 调用,从服务器拉取 todos client.get('todos').then(todos => { // 用接收到的 todos dispatch 一个 action storeAPI.dispatch({ type: 'todos/todosLoaded', payload: todos }) }) } return next(action) }

注意这两个中间件的写法:都是"三层嵌套函数"——最外层接收storeAPI,中间层接收next,最内层接收action。这与本仓库src/types/middleware.tsMiddleware类型的定义完全对应:

// src/types/middleware.ts(节选) export interface MiddlewareAPI<D extends Dispatch = Dispatch, S = any> { dispatch: D getState: () => S } export interface Middleware< _DispatchExt = {}, S = any, D extends Dispatch = Dispatch > { ( api: MiddlewareAPI<D, S> ): (next: (action: unknown) => unknown) => (action: unknown) => unknown }

也就是说,中间件的契约就是(api) => (next) => (action) => 任意返回值,其中最内层函数的参数类型是unknown——这从类型层面就承认了:dispatch 进来的一切都不保证是普通 action 对象,拦截非对象值(比如函数)正是中间件的合法职责。

源码视角:applyMiddleware 如何组装中间件管线

以上"三层嵌套函数"最终是由applyMiddleware串成管线的。看本仓库的实现 src/applyMiddleware.ts:

export default function applyMiddleware(...middlewares: Middleware[]): StoreEnhancer<any> { return createStore => (reducer, preloadedState) => { const store = createStore(reducer, preloadedState) let dispatch: Dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) } const middlewareAPI: MiddlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose<typeof dispatch>(...chain)(store.dispatch) return { ...store, dispatch } } }

从这段源码可以读出几个关键事实:

  1. applyMiddleware本身是一个 store enhancer:它返回createStore => (reducer, preloadedState) => store这样的高阶函数,最终返回一个"覆盖了dispatch的新 store"。这也解释了教程第 4 篇中"中间件是构建在一个非常特殊的内建 store enhancer 之上的"这句话。函数注释中还特别提示:由于中间件可能是异步的,applyMiddleware应当是 enhancer 组合链中的第一个。
  2. 每个中间件拿到的dispatch是"指向管线入口"的递归引用middlewareAPI.dispatch调用的是闭包中会被重新赋值的dispatch变量,最终被赋值为组合后的完整管线。因此中间件内部再 dispatch 的动作,会从第一个中间件重新走完整条管线,而不会被漏掉。
  3. 管线的组装发生在构建期middlewares.map(middleware => middleware(middlewareAPI))先对每个中间件调用最外层函数,拿到各自的wrapDispatch;再compose(...chain)(store.dispatch)把它们从右向左嵌套,把原始的store.dispatch作为链尾(即最后一个中间件的next)。compose的实现见 src/compose.ts,即compose(f, g, h) === (...args) => f(g(h(...args)))——这决定了applyMiddleware(a, b)时,a是管线最外层、最先执行,b最后执行并紧邻真实store.dispatch
  4. 构建期禁止 dispatch:在中间件装配完成之前,dispatch被临时指向一个会抛出 "Dispatching while constructing your middleware is not allowed" 的函数(src/applyMiddleware.ts),防止你在中间件外层函数里 dispatch 时绕过尚未装配的管线。

这些行为都有测试背书,见 test/applyMiddleware.spec.ts:

  • warns when dispatching during middleware setup(L9-L20)验证了第 4 点的抛错行为;
  • wraps dispatch method with middleware once(L22-L45)验证中间件外层函数只被调用一次,且拿到的 API 上确实有getStatedispatch
  • passes recursive dispatches through the middleware chain(L47-L65)验证"中间件内部 dispatch 的动作会重新经过整条管线"。

编写一个"异步函数"中间件

上一节的两个中间件都太具体,各只干一件事。我们当然希望有一种办法:把任意异步逻辑提前写成一个独立函数(与中间件定义解耦),同时这个函数还能访问dispatchgetState来与 store 交互。

如果我们写一个允许向dispatch传一个"函数"(而不是 action 对象)的中间件呢?这个中间件检查一下"动作"是否其实是函数,如果是,就立刻调用它。这样异步逻辑就能写在中间件定义之外的独立函数里。这样的中间件大概长这样:

// 示例异步函数中间件 const asyncFunctionMiddleware = storeAPI => next => action => { // 如果这个"action"其实是个函数…… if (typeof action === 'function') { // 那就调用它,并把 `dispatch` 和 `getState` 作为参数传入 return action(storeAPI.dispatch, storeAPI.getState) } // 否则,它是一个普通 action——继续往下传 return next(action) }

有了它,我们就可以这样使用:

const middlewareEnhancer = applyMiddleware(asyncFunctionMiddleware) const store = createStore(rootReducer, middlewareEnhancer) // 写一个以 `dispatch` 和 `getState` 为参数的函数 const fetchSomeData = (dispatch, getState) => { // 发起一次异步 HTTP 请求 client.get('todos').then(todos => { // 用接收到的 todos dispatch 一个 action dispatch({ type: 'todos/todosLoaded', payload: todos }) // dispatch 之后读取更新后的 store 状态 const allTodos = getState().todos console.log('Number of todos after loading: ', allTodos.length) }) } // 把上面写的"函数"传给 `dispatch` store.dispatch(fetchSomeData) // 日志输出:'Number of todos after loading: ###'

注意:这个"异步函数中间件"让我们向dispatch传入了一个函数。在函数内部,我们先写了一段异步逻辑(HTTP 请求),请求完成后再 dispatch 一个普通的 action 对象。

这里有一个容易忽略的边界:如果没有中间件拦截,把函数传给dispatch会在 store 的原始dispatch里直接失败。从源码看,src/createStore.ts 中对 action 的校验会检查action.type是否为字符串,非普通对象的值根本无法通过 reducer 校验流程;仓库也提供了 src/utils/isAction.ts 这样的工具函数,其判定标准就是"是纯对象且type为字符串"。换言之,函数型 dispatch 之所以可行,完全依赖于中间件先于 store 内部逻辑拦截了它——这正是文档所说"只要中间件拦截该值、不让它到达 reducer"的底层依据。

本仓库的测试辅助代码里就有一个与上面asyncFunctionMiddleware几乎一模一样的最小 thunk 实现,见 test/helpers/middleware.ts:

export const thunk: Middleware<{ <R>(thunk: (dispatch: Dispatch, getState: () => any) => R): R }> = ({ dispatch, getState }) => next => action => typeof action === 'function' ? action(dispatch, getState) : next(action)

而 test/applyMiddleware.spec.ts 中名为works with thunk middleware的测试用例,就用它验证了完整闭环:dispatch 一个 thunk 函数后,thunk 内部 dispatch 的普通 action 能正确更新 store 状态。

Redux 的异步数据流

那么,中间件和异步逻辑对 Redux 应用的整体数据流有什么影响?

和普通 action 一样,我们首先要处理一个用户事件(比如点击按钮),然后调用dispatch(),传入某种东西——一个纯 action 对象、一个函数,或其他可以被中间件查找识别的值。

这个被 dispatch 的值到达中间件后,中间件可以发起一次异步调用,等异步调用完成后再 dispatch 一个真正的 action 对象。

在 第 2 篇 中,我们见过表示常规同步 Redux 数据流的示意图。当 Redux 应用加入异步逻辑后,就多了中间件运行 HTTP 请求等逻辑、再 dispatch action 的额外环节。异步数据流看起来就是文章开头那张图:UI 事件触发dispatch,值进入中间件管线,中间件在异步等待(HTTP 请求、定时器等)结束后 dispatch 真实 action,action 再经过剩余中间件到达 reducer,状态更新后通知 UI 重新渲染。

使用 Redux Thunk 中间件

实际上,Redux 官方早就有了那个"异步函数中间件"的标准版本,叫做Redux "Thunk" 中间件(npm 包redux-thunk)。thunk 中间件让我们编写以dispatchgetState为参数的函数。thunk 函数内部可以包含我们想要的任何异步逻辑,这些逻辑可以按需 dispatch action、读取 store 状态。

把异步逻辑写成 thunk 函数,让我们可以在事先不知道将使用哪个 Redux store 的情况下复用这些逻辑。

术语说明:"thunk" 是一个编程术语,指"一段执行延迟工作(delayed work)的代码"。更多用法可参考 Writing Logic with Thunks 使用指南。

配置 Store

Redux thunk 中间件作为 npm 包redux-thunk发布,需要先安装:

npm install redux-thunk

安装后,更新 todo 应用的 Redux store 以使用这个中间件:

import { createStore, applyMiddleware } from 'redux' // highlight-next-line import { thunk } from 'redux-thunk' import { composeWithDevTools } from 'redux-devtools-extension' import rootReducer from './reducer' // highlight-next-line const composedEnhancer = composeWithDevTools(applyMiddleware(thunk)) // store 现在具备了在 `dispatch` 中接受 thunk 函数的能力 const store = createStore(rootReducer, composedEnhancer) export default store

composeWithDevTools(applyMiddleware(thunk))正是把applyMiddleware返回的 enhancer 交给 DevTools 组合函数再传给createStore,对应 src/applyMiddleware.ts 中 "enhancer 包 enhancer" 的高阶结构。

从服务器获取 Todos

现在我们的 todo 条目只能存在于客户端浏览器里。我们首先需要一种方式,在应用启动时从服务器加载待办列表。

先写一个 thunk 函数:它发起 HTTP 调用请求/fakeApi/todos端点、取得 todo 对象数组,然后 dispatch 一个以该数组为 payload 的 action。由于这与 todos 功能整体相关,thunk 函数写在todosSlice.js里(示例应用文件):

import { client } from '../../api/client' const initialState = [] export default function todosReducer(state = initialState, action) { // 省略 reducer 逻辑 } // Thunk 函数 // highlight-start export async function fetchTodos(dispatch, getState) { const response = await client.get('/fakeApi/todos') dispatch({ type: 'todos/todosLoaded', payload: response.todos }) } // highlight-end

这个 API 调用只希望在应用第一次加载时执行一次。可以放的地方有好几处:

  • <App>组件的useEffecthook 里;
  • <TodoList>组件的useEffecthook 里;
  • 直接在index.js里,紧跟在导入 store 之后。

这里先试放在index.js里:

import React from 'react' import { createRoot } from 'react-dom/client' import { Provider } from 'react-redux' import './index.css' import App from './App' import './api/server' // highlight-start import store from './store' import { fetchTodos } from './features/todos/todosSlice' store.dispatch(fetchTodos) // highlight-end const root = createRoot(document.getElementById('root')) root.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> )

刷新页面后,UI 上没有可见变化。但如果打开 Redux DevTools 扩展,应该能看到一个'todos/todosLoaded'动作被 dispatch 了,其中包含由我们的假服务器 API 生成的一些 todo 对象:

注意:虽然我们已经 dispatch 了动作,但状态并没有发生任何变化。我们需要在 todos reducer 中处理这个动作,状态才会更新。

给 reducer 加一个 case 来把数据加载进 store。因为数据是从服务器取来的,我们要完全替换已有的 todos,所以直接返回action.payload数组,让它成为 todos 的新state值:

import { client } from '../../api/client' const initialState = [] export default function todosReducer(state = initialState, action) { switch (action.type) { // 省略其他 reducer case // highlight-start case 'todos/todosLoaded': { // 直接返回新值,整体替换现有 state return action.payload } // highlight-end default: return state } } export async function fetchTodos(dispatch, getState) { const response = await client.get('/fakeApi/todos') dispatch({ type: 'todos/todosLoaded', payload: response.todos }) }

由于 dispatch 一个动作会立刻更新 store,我们也可以在 thunk 里调用getState,在 dispatch 之后读取更新后的状态值。例如,在 dispatch'todos/todosLoaded'动作前后各打印一次 todo 总数:

export async function fetchTodos(dispatch, getState) { const response = await client.get('/fakeApi/todos') // highlight-next-line const stateBefore = getState() console.log('Todos before dispatch: ', stateBefore.todos.length) dispatch({ type: 'todos/todosLoaded', payload: response.todos }) // highlight-next-line const stateAfter = getState() console.log('Todos after dispatch: ', stateAfter.todos.length) }

这正体现了中间件 API 中getState的价值:从 src/applyMiddleware.ts 可以看到,middlewareAPI.getState直接引用了 store 的getState,因此 thunk 内部读到的永远是当下最新的状态。

保存 Todo 条目

接下来,每当创建新待办条目时,我们也需要更新服务器。正确做法是:不要立即 dispatch'todos/todoAdded'动作,而是先向服务器发起一次携带初始数据的 API 调用,等待服务器返回新保存的 todo 条目副本,然后再用这个 todo 条目 dispatch 动作。

但如果直接把它写成一个 thunk 函数,会立刻遇到一个问题:thunk 是写在todosSlice.js里的独立函数,发起 API 调用的代码并不知道新 todo 的文本是什么

async function saveNewTodo(dispatch, getState) { // ❌ 我们需要新 todo 的文本,但它从哪来? // highlight-next-line const initialTodo = { text } const response = await client.post('/fakeApi/todos', { todo: initialTodo }) dispatch({ type: 'todos/todoAdded', payload: response.todo }) }

我们需要一种方式:写一个接收text参数的函数,由它创建真正的 thunk 函数,使得 thunk 能利用text值发起 API 调用。外层函数返回这个 thunk 函数,让我们可以把它传给组件里的dispatch

// 写一个同步的外层函数,接收 `text` 参数: export function saveNewTodo(text) { // 然后创建并返回异步 thunk 函数: return async function saveNewTodoThunk(dispatch, getState) { // ✅ 现在可以用 text 值并把它发送到服务器 const initialTodo = { text } const response = await client.post('/fakeApi/todos', { todo: initialTodo }) dispatch({ type: 'todos/todoAdded', payload: response.todo }) } }

现在可以在<Header>组件中使用它:

import React, { useState } from 'react' import { useDispatch } from 'react-redux' // highlight-next-line import { saveNewTodo } from '../todos/todosSlice' const Header = () => { const [text, setText] = useState('') const dispatch = useDispatch() const handleChange = e => setText(e.target.value) const handleKeyDown = e => { // 如果用户按下了回车键: const trimmedText = text.trim() if (e.which === 13 && trimmedText) { // highlight-start // 用用户输入的文本创建 thunk 函数 const saveNewTodoThunk = saveNewTodo(trimmedText) // 然后把 thunk 函数本身 dispatch 出去 dispatch(saveNewTodoThunk) // highlight-end setText('') } } // 省略渲染输出 }

由于我们清楚自己会立刻把 thunk 函数传给组件中的dispatch,可以省去那个临时变量:直接调用saveNewTodo(text),把得到的 thunk 函数原样传给dispatch

const handleKeyDown = e => { // 如果用户按下了回车键: const trimmedText = text.trim() if (e.which === 13 && trimmedText) { // highlight-start // 创建 thunk 函数并立刻 dispatch dispatch(saveNewTodo(trimmedText)) // highlight-end setText('') } }

此时组件实际上并不知道自己在 dispatch 一个 thunk 函数——saveNewTodo函数封装了真正发生的事情。<Header>组件只知道:用户按回车时,它需要 dispatch某个值

这种"写一个函数来准备将要传给dispatch的东西"的模式,就是所谓的"action creator" 模式,在 Part 7: 标准 Redux 模式 中会进一步展开。

现在可以看到更新后的'todos/todoAdded'动作被 dispatch:

最后需要修改的是 todos reducer。当我们向/fakeApi/todos发起 POST 请求时,服务器会返回一个全新的 todo 对象(包含新的 ID 值)。这意味着 reducer 不必自己计算新 ID、也不必填其他字段——它只需要构造一个包含新 todo 条目的新state数组

const initialState = [] export default function todosReducer(state = initialState, action) { switch (action.type) { // highlight-start case 'todos/todoAdded': { // 返回一个新的 todos 状态数组,新 todo 条目追加在末尾 return [...state, action.payload] } // highlight-end // 省略其他 case default: return state } }

这样,新增 todo 就完全正常工作了,状态 diff 如下:

提示:thunk 函数既能用于异步逻辑,也能用于同步逻辑。thunk 提供了一种方式来编写任何需要访问dispatchgetState的可复用逻辑。

本教程小结

到目前为止,我们已经成功更新了 todo 应用:可以用 "thunk" 函数向假服务器 API 发起 HTTP 请求,从而获取待办列表并保存新的待办条目。

在这个过程中,我们看到了 Redux 中间件是如何让我们发起异步调用、并在异步调用完成后通过 dispatch 动作与 store 交互的。核心结论回顾:

  • Redux 中间件被设计用于编写带副作用的逻辑
    • "副作用"是改变函数外部状态/行为的代码,例如 HTTP 请求、修改函数参数、生成随机值;
  • 中间件在标准 Redux 数据流中加了一个额外环节
    • 中间件可以拦截被传给dispatch的其他值;
    • 中间件可以访问dispatchgetState,因此可以在异步逻辑中 dispatch 更多动作;
  • Redux "Thunk" 中间件让我们可以向dispatch传函数
    • "thunk" 函数让我们可以提前写好异步逻辑,而无需事先知道将使用哪个 Redux store;
    • Redux thunk 函数接收dispatchgetState作为参数,可以 dispatch "已从 API 响应中收到这些数据"之类的动作。

从本仓库源码看,这套机制的骨架非常紧凑:applyMiddleware(src/applyMiddleware.ts)以 enhancer 形式把中间件链组合成新的dispatchMiddleware/MiddlewareAPI类型(src/types/middleware.ts)定义了三层嵌套函数契约与dispatch/getState两个能力,compose(src/compose.ts)完成从右到左的管线嵌套,而 test/applyMiddleware.spec.ts 中的用例则逐一验证了构建期禁 dispatch、中间件仅装配一次、内部 dispatch 重走管线、以及与 thunk 配合的完整行为。

下一步

到这里,我们已经覆盖了使用 Redux 的所有核心部分:

  • 编写根据 dispatch 的动作更新状态的 reducer;
  • 用 reducer、enhancer 和中间件创建并配置 Redux store;
  • 使用中间件编写会 dispatch 动作的异步逻辑。

在 Part 7: 标准 Redux 模式 中,我们将研究真实 Redux 应用常用的若干代码模式,让代码更一致,并随应用增长更好地扩展。

【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

Wand-Enhancer完整指南:免费解锁WeMod Pro高级功能

Wand-Enhancer完整指南&#xff1a;免费解锁WeMod Pro高级功能 【免费下载链接】Wand-Enhancer Advanced UX and interoperability extension for Wand (WeMod) app 项目地址: https://gitcode.com/GitHub_Trending/we/Wand-Enhancer WeMod 的高级功能都被 Pro 订阅锁着…

作者头像 李华
网站建设 2026/9/5 16:31:51

Skyvern 浏览器自动化平台:如何 10 分钟跑通第一条 AI 工作流

Skyvern 浏览器自动化平台&#xff1a;如何 10 分钟跑通第一条 AI 工作流 【免费下载链接】skyvern Automate browser based workflows with AI 项目地址: https://gitcode.com/GitHub_Trending/sk/skyvern Skyvern 是一个开源的浏览器自动化平台&#xff0c;你给它一句…

作者头像 李华
网站建设 2026/9/5 16:21:35

LLM基准审计工具BenchMIRT:破解评测数据污染与模型记忆难题

这次我们来看 Hugging Face 与 Ai2 联合推出的一个专门做“基准审计”的工具&#xff1a;BenchMIRT。它解决的问题非常具体&#xff1a;当大家都在用 MMLU、GPQA 这类 benchmark 给大模型打分时&#xff0c;模型刷的到底是“推理能力”&#xff0c;还是“题目记忆”&#xff1f…

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

禁闭求生2恶意盛宴获取教程:神秘人营地触发条件与起源荆棘全流程攻略

如果你在《禁闭求生2》里已经推进到“由内而外畸形藤蔓”事件&#xff0c;却一直没见到神秘人布莱顿布莱尔&#xff0c;那很可能是同一个原因&#xff1a;你把地图上的世界创伤处理得太干净了。神秘人营地不会无限期刷在地图上&#xff0c;他的出现和“由内而外”的存活状态有直…

作者头像 李华