普通视图

发现新文章,点击刷新页面。
今天 — 2026年5月14日首页

Bash Best Practices: Writing Safer, Cleaner Scripts

Bash scripts tend to start small and grow over time. A helper you wrote in five minutes to copy a few files can end up running in a cron job, a deployment pipeline, or a server startup script. When that happens, small oversights such as an unquoted variable or an ignored error become real outages.

This guide covers practical Bash best practices that keep scripts predictable, safe to re-run, and easy for other people to read.

Start With a Clear Shebang

Every Bash script should begin with a proper shebang line. The portable choice for Bash is:

~/script.shsh
#!/usr/bin/env bash

Using /usr/bin/env bash lets the script find Bash through the PATH, which matters on systems where Bash is installed outside /bin, such as macOS with Homebrew. If you rely on features that exist only in Bash, do not use #!/bin/sh. On many distributions /bin/sh points to dash or another minimal shell, and arrays, [[ ]], and local will not behave the same way.

Enable Strict Mode

By default, Bash ignores many errors. A command can fail, a variable can be unset, and the script will keep running as if nothing happened. Enabling strict mode at the top of the script changes that behavior.

~/script.shsh
#!/usr/bin/env bash
set -euo pipefail

Here is what each flag does:

  • -e - Exit immediately if any command returns a non-zero status.
  • -u - Treat unset variables as an error and exit.
  • -o pipefail - Make a pipeline fail if any command in it fails, not only the last one.

A common extra is IFS=$'\n\t', which prevents word splitting on spaces and avoids surprises when iterating over filenames that contain spaces.

~/script.shsh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

Strict mode is not magic. It will not catch logic errors, and in some scripts you will need to opt out locally with || true for commands that are allowed to fail. The point is to flip the default from “keep going on errors” to “stop on errors”, which is almost always the safer choice.

Always Quote Your Variables

Unquoted variables in Bash are expanded and then word-split. If a variable contains spaces, wildcards, or is empty, the result may be very different from what you expect.

Terminal
file="My Report.txt"
printf '<%s>\n' $file

Because $file is not quoted, Bash passes two arguments to printf: My and Report.txt. Quoting fixes it and keeps the filename as one value:

Terminal
printf '<%s>\n' "$file"

The same rule applies inside tests, loops, and command substitutions. When in doubt, quote. The only common case where you intentionally leave a variable unquoted is when you want word splitting, which should be rare and deliberate.

Prefer [[ ]] Over [ ]

Bash supports two test constructs: the older [ ] (a synonym for the test command) and the newer [[ ]] keyword. For Bash scripts, prefer [[ ]]:

sh
if [[ -f "$config" && "$env" == "production" ]]; then
 echo "Loading production config"
fi

[[ ]] handles empty variables safely even without quotes, supports pattern matching with == and regex with =~, and allows logical operators such as && and || inside the test. Only fall back to [ ] when writing a portable POSIX script that must run under sh or dash.

Use Functions for Repeated Logic

Once a script has more than a couple of steps, group related commands into functions. Functions make scripts easier to read, test, and refactor.

~/backup.shsh
log() {
 printf '[%s] %s\n' "$(date +'%F %T')" "$*"
}

backup_dir() {
 local src="$1"
 local dst="$2"

 log "Backing up $src to $dst"
 rsync -a "$src/" "$dst/"
}

backup_dir "/var/www" "/backup/www"

Declare variables inside functions with local so they do not leak into the rest of the script. Pass input as positional parameters, and return output via echo or printf rather than global variables when possible.

See Bash functions for more details on arguments, return values, and scoping.

Handle Errors Explicitly

Strict mode stops the script on errors, but it does not tell the user what went wrong. Adding a trap on ERR gives you a single place to log failures.

~/script.shsh
#!/usr/bin/env bash
set -Eeuo pipefail

on_error() {
 local exit_code=$?
 local line=$1
 echo "Error on line $line (exit code $exit_code)" >&2
 exit "$exit_code"
}

trap 'on_error $LINENO' ERR

The -E option makes the ERR trap inherit into functions and subshells. Without it, a failure inside a helper function may exit the script without running your error handler.

For cleanup tasks such as removing temporary files, use trap on EXIT:

~/script.shsh
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT

The EXIT trap runs whether the script exits normally, hits an error under set -e, or is interrupted with Ctrl+C. It is the safest way to guarantee cleanup.

Validate Input Early

Scripts that take arguments should fail fast when those arguments are missing or invalid. Check them before doing any work:

~/deploy.shsh
if [[ $# -lt 2 ]]; then
 echo "Usage: $0 <environment> <version>" >&2
 exit 1
fi

env="$1"
version="$2"

case "$env" in
 staging|production) ;;
 *)
 echo "Unknown environment: $env" >&2
 exit 1
 ;;
esac

For scripts with several flags, use getopts instead of parsing $@ manually. It handles short options, arguments, and error reporting in a consistent way.

Avoid Parsing the Output of ls

A common beginner mistake is looping over ls output:

sh
for f in $(ls /var/log); do
 echo "$f"
done

This breaks on filenames with spaces, newlines, or special characters. Use a glob or find instead:

sh
for f in /var/log/*; do
 echo "$f"
done

For recursive traversal, use find with -print0 and xargs -0, or a while read loop with IFS= and -d '' to handle unusual filenames correctly.

Keep Scripts Idempotent When Possible

A script you can safely re-run is much easier to work with than one that breaks on the second attempt. Aim for idempotent behavior: check before creating directories, use mkdir -p instead of mkdir, use ln -sf instead of ln -s, and guard commands with if blocks when they would otherwise fail on re-run.

Terminal
mkdir -p /opt/app/config

The same pattern applies to package installation, user creation, and configuration edits. Tools like Ansible enforce idempotency by design; in Bash, you have to add those checks yourself.

Use Shellcheck

Shellcheck is a static analyzer for shell scripts. It catches unquoted variables, unsafe for loops, and many other common mistakes before you hit them in production.

Install it from your package manager:

Terminal
sudo apt install shellcheck

Then run it against your script:

Terminal
shellcheck script.sh

Fix the warnings it raises, or document with an inline comment why a specific check is being ignored. Making shellcheck part of your editor or pre-commit hook removes an entire class of Bash bugs.

Prefer Readable Over Clever

Bash is full of shortcuts and obscure expansions. They can make scripts shorter, but they also make them harder to read six months later. Choose the clearer form, even when it is a few characters longer.

sh
if [[ -z "$name" ]]; then
 name="anonymous"
fi

is almost always easier to maintain than the golfed equivalent:

Terminal
: "${name:=anonymous}"

Aim for scripts that a teammate can read once and understand, not ones that require a reference manual.

Quick Reference

For a printable quick reference, see the Bash cheatsheet .

Before committing a Bash script, confirm the following:

  • #!/usr/bin/env bash shebang is present.
  • set -euo pipefail is enabled.
  • All variable expansions are quoted.
  • Tests use [[ ]] instead of [ ].
  • Functions replace long repeated blocks, with local variables.
  • trap handles errors and cleanup.
  • Input is validated before work begins.
  • No parsing of ls output.
  • shellcheck reports no warnings, or they are explicitly ignored.

FAQ

Is set -e enough on its own?
No. set -e stops on many errors, but it misses failures in pipelines and ignores unset variables. Combine it with -u and -o pipefail to cover those cases.

Does strict mode break existing scripts?
It often surfaces bugs that were already there, such as unset variables and ignored errors. The fix is to quote variables, use default values like ${VAR:-}, and handle expected failures with || true.

Should I write scripts in Bash or switch to Python?
Bash is a good fit for short glue scripts that mostly call other commands. Once a script grows past a few hundred lines, has complex data structures, or needs proper error handling and testing, Python or another language is usually a better choice.

Where can I learn more safe scripting patterns?
Keep the Shellcheck wiki open while you write, and read through the official Bash manual section on shell builtins. Both cover the reasoning behind each rule, not only the rule itself.

Conclusion

Strict mode, quoting, and a few well-placed traps cover most of the risk in Bash scripting. Make these practices your defaults, run shellcheck on every change, and your scripts will behave the same way on your laptop, in CI, and on a production server.

每日一题-检查数组是否是好的🟢

2026年5月14日 00:00

给你一个整数数组 nums ,如果它是数组 base[n] 的一个排列,我们称它是个  数组。

base[n] = [1, 2, ..., n - 1, n, n] (换句话说,它是一个长度为 n + 1 且包含 1 到 n - 1 恰好各一次,包含 n  两次的一个数组)。比方说,base[1] = [1, 1] ,base[3] = [1, 2, 3, 3] 。

如果数组是一个好数组,请你返回 true ,否则返回 false 。

注意:数组的排列是这些数字按任意顺序排布后重新得到的数组。

 

示例 1:

输入:nums = [2, 1, 3]
输出:false
解释:因为数组的最大元素是 3 ,唯一可以构成这个数组的 base[n] 对应的 n = 3 。但是 base[3] 有 4 个元素,但数组 nums 只有 3 个元素,所以无法得到 base[3] = [1, 2, 3, 3] 的排列,所以答案为 false 。

示例 2:

输入:nums = [1, 3, 3, 2]
输出:true
解释:因为数组的最大元素是 3 ,唯一可以构成这个数组的 base[n] 对应的 n = 3 ,可以看出数组是 base[3] = [1, 2, 3, 3] 的一个排列(交换 nums 中第二个和第四个元素)。所以答案为 true 。

示例 3:

输入:nums = [1, 1]
输出:true
解释:因为数组的最大元素是 1 ,唯一可以构成这个数组的 base[n] 对应的 n = 1,可以看出数组是 base[1] = [1, 1] 的一个排列。所以答案为 true 。

示例 4:

输入:nums = [3, 4, 4, 1, 2, 1]
输出:false
解释:因为数组的最大元素是 4 ,唯一可以构成这个数组的 base[n] 对应的 n = 4 。但是 base[n] 有 5 个元素而 nums 有 6 个元素。所以答案为 false 。

 

提示:

  • 1 <= nums.length <= 100
  • 1 <= num[i] <= 200

2784. 检查数组是否是好的

作者 stormsunshine
2024年1月26日 07:07

解法一

思路和算法

记 $n$ 为数组 $\textit{nums}$ 的长度减 $1$。数组 $\textit{nums}$ 是好数组,等价于将数组 $\textit{nums}$ 按升序排序之后,应满足对于所有 $0 \le i < n$ 都有 $\textit{nums}[i] = i + 1$ 且 $\textit{nums}[n - 1] = \textit{nums}[n] = n$,因此可以将数组 $\textit{nums}$ 按升序排序并判断数组 $\textit{nums}$ 是否为好数组。

排序之后,需要检查 $\textit{nums}[n] = n$、$\textit{nums}[n - 1] = n$ 和对于所有 $0 \le i < n$ 都有 $\textit{nums}[i] = i + 1$ 是否成立,如果上述条件成立,则数组 $\textit{nums}$ 是好数组,返回 $\text{true}$,否则返回 $\text{false}$。

代码

###Java

class Solution {
    public boolean isGood(int[] nums) {
        int n = nums.length - 1;
        Arrays.sort(nums);
        if (nums[n] != n || nums[n - 1] != n) {
            return false;
        }
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                return false;
            }
        }
        return true;
    }
}

###C#

public class Solution {
    public bool IsGood(int[] nums) {
        int n = nums.Length - 1;
        Array.Sort(nums);
        if (nums[n] != n || nums[n - 1] != n) {
            return false;
        }
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                return false;
            }
        }
        return true;
    }
}

复杂度分析

  • 时间复杂度:$O(m \log m)$,其中 $m$ 是数组 $\textit{nums}$ 的长度。排序的时间是 $O(m \log m)$,排序后遍历数组的时间是 $O(m)$,因此时间复杂度是 $O(m \log m)$。

  • 空间复杂度:$O(\log m)$,其中 $m$ 是数组 $\textit{nums}$ 的长度。排序的递归调用栈空间是 $O(\log m)$。

解法二

思路和算法

记 $n$ 为数组 $\textit{nums}$ 的长度减 $1$。数组 $\textit{nums}$ 是好数组,等价于数组 $\textit{nums}$ 中的元素都不超过 $n$ 且 $1$ 到 $n - 1$ 的每个整数都恰好出现一次。因此可以使用哈希集合存储数组 $\textit{nums}$ 中出现的 $1$ 到 $n - 1$ 的每个整数,遍历数组 $\textit{nums}$ 的过程中维护哈希集合并判断数组 $\textit{nums}$ 是否为好数组。

对于遍历到的每个元素 $\textit{nums}$,执行如下操作。

  1. 如果 $\textit{num} > n$,则数组 $\textit{nums}$ 不是好数组,返回 $\text{false}$。

  2. 如果 $\textit{num} < n$。执行如下操作。

    • 如果 $\textit{num}$ 已经在哈希集合中,则 $\textit{num}$ 在数组 $\textit{nums}$ 中重复出现,因此数组 $\textit{nums}$ 不是好数组,返回 $\text{false}$。

    • 如果 $\textit{num}$ 尚未在哈希集合中,则将 $\textit{num}$ 加入哈希集合。

遍历结束之后,如果哈希集合中的元素个数等于 $n - 1$,则数组 $\textit{nums}$ 是好数组,返回 $\text{true}$,否则返回 $\text{false}$。

上述做法中没有判断 $n$ 的出现次数,理由如下。

  1. 如果数组 $\textit{nums}$ 中存在大于 $n$ 的元素,则数组 $\textit{nums}$ 不是好数组。

  2. 如果数组 $\textit{nums}$ 中不存在大于 $n$ 的元素,则由于数组 $\textit{nums}$ 的长度是 $n + 1$,因此只需要确保 $1$ 到 $n - 1$ 的每个整数都出现一次,不存在重复或遗漏,则可以确保 $n$ 出现两次。上述做法中,遍历数组执行哈希集合操作时检查是否存在重复元素,遍历结束之后检查是否存在遗漏元素,因此只有当 $1$ 到 $n - 1$ 的每个整数都出现一次时才会认为数组 $\textit{nums}$ 是好数组。

代码

###Java

class Solution {
    public boolean isGood(int[] nums) {
        int n = nums.length - 1;
        Set<Integer> set = new HashSet<Integer>();
        for (int num : nums) {
            if (num > n) {
                return false;
            }
            if (num < n && !set.add(num)) {
                return false;
            }
        }
        return set.size() == n - 1;
    }
}

###C#

public class Solution {
    public bool IsGood(int[] nums) {
        int n = nums.Length - 1;
        ISet<int> set = new HashSet<int>();
        foreach (int num in nums) {
            if (num > n) {
                return false;
            }
            if (num < n && !set.Add(num)) {
                return false;
            }
        }
        return set.Count == n - 1;
    }
}

复杂度分析

  • 时间复杂度:$O(m)$,其中 $m$ 是数组 $\textit{nums}$ 的长度。需要遍历数组一次,对于每个元素的操作时间都是 $O(1)$。

  • 空间复杂度:$O(m)$,其中 $m$ 是数组 $\textit{nums}$ 的长度。哈希集合的空间是 $O(m)$。

排序 + 遍历

2023年8月7日 12:19

Problem: 2784. 检查数组是否是好的

[TOC]

思路

排序 + 一次遍历

解题方法

排序后计算数组长度,如果数组最后一个值大于等于当前数组长度,直接返回False。依次遍历数组起始至倒数第二个值下标与值是否相等,不相等返回False。

复杂度

  • 时间复杂度:

添加时间复杂度, 示例: $O(n)$

  • 空间复杂度:

添加空间复杂度, 示例: $O(n)$

Code

###Python3


class Solution:
    def sort_ergo(self, arr):
        size = len(arr)
        arr.sort()
        if arr[-1] >= size:
            return False
        for indx, val in enumerate(arr[:size-1], 1):
            if indx != val:
                return False
        return True

    def isGood(self, nums: List[int]) -> bool:
        # 方法:排序 + 遍历
        return self.sort_ergo(nums)

两种方法:辅助数组 / O(1) 空间(Python/Java/C++/Go)

作者 endlesscheng
2023年7月23日 09:00

方法一:辅助数组

遍历 $\textit{nums}$,同时用一个 $\textit{cnt}$ 数组统计每个元素的出现次数:

设 $n$ 是 $\textit{nums}$ 的长度减一。设 $x = \textit{nums}[i]$。分类讨论:

  • 如果 $x > n$,不满足要求,返回 $\texttt{false}$。注意题目保证 $x\ge 1$,无需判断 $x\le 0$ 的情况。
  • 如果 $x = n$ 且 $x$ 出现次数大于 $2$,返回 $\texttt{false}$。
  • 如果 $x < n$ 且 $x$ 出现次数大于 $1$,返回 $\texttt{false}$。

如果没有出现上述情况,返回 $\texttt{true}$。

class Solution:
    def isGood(self, nums: List[int]) -> bool:
        n = len(nums) - 1
        cnt = [0] * (n + 1)
        for x in nums:
            if (x > n or
                x == n and cnt[x] > 1 or  # cnt[x] 加一之前 > 1,加一之后 > 2
                x < n and cnt[x] > 0):    # cnt[x] 加一之前 > 0,加一之后 > 1
                return False
            cnt[x] += 1
        return True
class Solution {
    public boolean isGood(int[] nums) {
        int n = nums.length - 1;
        int[] cnt = new int[n + 1];
        for (int x : nums) {
            if (x > n ||
                x == n && cnt[x] > 1 || // cnt[x] 加一之前 > 1,加一之后 > 2
                x < n && cnt[x] > 0) {  // cnt[x] 加一之前 > 0,加一之后 > 1
                return false;
            }
            cnt[x]++;
        }
        return true;
    }
}
class Solution {
public:
    bool isGood(vector<int>& nums) {
        int n = nums.size() - 1;
        vector<int> cnt(n + 1);
        for (int x : nums) {
            if (x > n ||
                x == n && cnt[x] > 1 || // cnt[x] 加一之前 > 1,加一之后 > 2
                x < n && cnt[x] > 0) {  // cnt[x] 加一之前 > 0,加一之后 > 1
                return false;
            }
            cnt[x]++;
        }
        return true;
    }
};
func isGood(nums []int) bool {
n := len(nums) - 1
cnt := make([]int, n+1)
for _, x := range nums {
if x > n ||
x == n && cnt[x] > 1 || // cnt[x] 加一之前 > 1,加一之后 > 2
x < n && cnt[x] > 0 {   // cnt[x] 加一之前 > 0,加一之后 > 1
return false
}
cnt[x]++
}
return true
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

方法二:把 nums 当作辅助数组

由于 $\textit{nums}$ 中的数都是正整数,我们可以在首次遇到元素 $x$ 时,把 $\textit{nums}[x]$ 改成 $-\textit{nums}[x]$,这样再次遇到 $|x|$ 时,就能通过 $\textit{nums}[|x|] < 0$ 得知 $\textit{nums}$ 中至少有两个 $|x|$。

对于 $n$,我们需要判断 $n$ 是否出现超过两次,可以单独用一个变量 $\textit{cntN}$ 统计 $n$ 的出现次数。

class Solution:
    def isGood(self, nums: List[int]) -> bool:
        n = len(nums) - 1
        cnt_n = 0
        for x in nums:
            x = abs(x)
            if (x > n or
                x == n and cnt_n > 1 or
                x < n and nums[x] < 0):  # x 之前遇到过,现在又遇到了,所以 x 的出现次数至少是 2
                return False
            if x == n:
                cnt_n += 1
            else:
                nums[x] = -nums[x]  # 标记 x 遇到过
        return True
class Solution {
    public boolean isGood(int[] nums) {
        int n = nums.length - 1;
        int cntN = 0;
        for (int x : nums) {
            x = Math.abs(x);
            if (x > n ||
                x == n && cntN > 1 ||
                x < n && nums[x] < 0) { // x 之前遇到过,现在又遇到了,所以 x 的出现次数至少是 2
                return false;
            }
            if (x == n) {
                cntN++;
            } else {
                nums[x] = -nums[x]; // 标记 x 遇到过
            }
        }
        return true;
    }
}
class Solution {
public:
    bool isGood(vector<int>& nums) {
        int n = nums.size() - 1;
        int cnt_n = 0;
        for (int x : nums) {
            x = abs(x);
            if (x > n ||
                x == n && cnt_n > 1 ||
                x < n && nums[x] < 0) { // x 之前遇到过,现在又遇到了,所以 x 的出现次数至少是 2
                return false;
            }
            if (x == n) {
                cnt_n++;
            } else {
                nums[x] = -nums[x]; // 标记 x 遇到过
            }
        }
        return true;
    }
};
func isGood(nums []int) bool {
n := len(nums) - 1
cntN := 0
for _, x := range nums {
x = abs(x)
if x > n ||
x == n && cntN > 1 ||
x < n && nums[x] < 0 { // x 之前遇到过,现在又遇到了,所以 x 的出现次数至少是 2
return false
}
if x == n {
cntN++
} else {
nums[x] = -nums[x] // 标记 x 遇到过
}
}
return true
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

相似题目

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

昨天 — 2026年5月13日首页

MiniMax 推出了 Mavis,活脱脱的 Agent「三省六部」

作者 张子豪
2026年5月13日 21:13

我下了一个任务,agent 开启了 plan 模式,规划了 7 个步骤。

我批准了,它开始跑,跑了三个步骤,然后停下来汇报:「我已经完成了 1、2、3,结果有这些和哪些……请问是否继续 4、5、6、7?」

我说继续。它又跑了两步,然后又停了下来:「我已经完成了 4、5,结果有这些和哪些……请问是否继续 6、7?」

一个晚上下来,让 agent 干点长程的任务,并没有长程的效果,对话框来回来去的全都是「继续」。

很长时间以来,我在使用各种 Agent 完成工作,就是这样的体验。

这种体验很不合逻辑。虽然「停下来确认」是个与 AI 共事时的好工作习惯,但在很多任务当中我从来没主动要求它停,但它就是会停下来。

MiniMax 在最新的技术博客文章中,将 agent 产品的这种行为归因于「上下文焦虑」。核心在于,模型本身对于「超长任务啥时候才算做完」的判断是模糊的。说白了,不是不会做,而是不敢做,每完成一步都怕做错,所以才会干一半就停下来问。

今天,MiniMax Agent 桌面端完成了一次重大更新。新加入了一个名为 Mavis 的模式(其实它是「MiniMax as a Jarvis」的缩写)。

要知道让一个 agent 当老板,一组 agent 当员工——这种传统的多 agent 框架已经不是什么新鲜事了。但 MiniMax 指出,此前的主流多 agent 框架,其实本质上就是靠提示词编排来让模型玩「角色扮演」role play。但这种做法撑不了多久,就会遇到包括前面提到的上下文焦虑、长程任务退化、自检等难题。

多 Agent 系统,需要一套持续运行、持续维护,并且多个 agent 之间不会「媾和」的可靠基础设施。这就是 MiniMax 在做的事。

实测体验:让 agent 给对方「挑刺」

MiniMax 给它的 Agent Team 基础设施起的名字叫做 Team Engine,引擎下面挂着三类核心角色:Leader、Worker、Verifier。顾名思义,一类做管理,一类干活,一类验收。

最关键的差异在于,Worker 和 Verifier 之间是「对抗」的关系,谁也没法蒙混过关。

前段时间,APPSO 正好在研究一个课题:「所有对 Coding/Agent 有所抱负的模型厂商,都要做自己的独立 Coding/Agent 产品」。

(没错,MiniMax 在此之前是个反面案例,但没想到文章还没发出来,就已经证明自己了!)

于是我们又用这个课题再在 MiniMax 的 Agent Team 上跑了一次。

这个任务拆分出了 5 个 worker,每个 worker 完成任务后,都会整理结果交给 leader(显示状态「Mavis 发给 General」或者「General 发给 Mavis」等等。)

有一个 worker,运行了 12 分钟还没有返回结果。APPSO 注意到,这个 leader等不及了,于是发了一条 bash 命令检查其工作状态:

在 5 个 worker 都完成后,leader 又生成了 5 个 verifier——在任务列表中显示为带着「小黄帽」的 agent:

Verifier 很快就找到了错误!其中一个 verifier 发现了对应的 worker 交付成果中存在明确的数据错误,给出了「失败」的判罚。紧接着,与之对应的 worker 重新启动(显示为运行中,会有一个蓝色小圈的标识)。

点进对应的 worker 工作区观察一下它的思考过程:「verifier 拒绝了我之前的交付成果,基于以下三个错误……我需要返回去重新核查关键事实,并检查修正具体的数字问题……」

还别说,agent 跟 agent 之间「铁面无私」,工作起来真的可靠。

这样的来来回回,在五组 1v1 的 agent 对抗当中,总共发生了数十次。过程中,Mavis 还表示这次「学到了新东西」,并顺手更新了一下记忆。

上一个任务先跑着,我们再开启一个新的深度研究,基于权威口径数据分析五一假期的旅游市场,并交付一份多维度分析报告。

这个研究比刚才的任务更加复杂。而且因为要持续对抗,Agent Team 在深度研究上所花的时间,也远比一般的单 Agent 要长。

但最终呈现的报告,和其它 AI 深度研究交付的内容相比起来,确实干净不少,也更加可信。

最近 APPSO 筹备了很多场线下活动,做策划想方案一直是个难题。我们也把这个任务交给 Mavis 看看效果怎么样。

我需要策划一场在广州举办的 AI 开发者线下沙龙,请你尽可能全面的给我提供多个适合百人千人科技活动的场地及大概报价,以及抓取同类活动的信息,然后帮我策划这张 AI 活动的主题,宣传,运营整个全部的工作,帮我把这些都整理成一份严格的商业计划书格式,以及一个符合主题特色,设计精美的网页。

光是制定计划的时间,就比之前的深度研究任务要长。Mavis 回复「这个任务规模很大,需要多个 Agent 并行工作——场地调研、竞品抓取、主题策划、商业计划书、网页开发。」

Mavis 的过人之处,就在于我们还可以持续追加新的需求:

给我长报告的同时,最好还能给我起草一份初步的正式合同,和场地的合作、以及和邀请嘉宾的合作、等等可能涉及的合同,还有前期的财务表格,再给我一份用来汇报这套方案的 PPT,越详细越好。

Agent Team 收到新需求后,会进一步完善计划并启动更多的工作流,最后,我们启动了多达 9 个并行任务。

我们点开 Mavis 的思考过程,能看到里面有大量的 agent 之间互相发送的消息,这些 Agents 会在专门的 Team Engine 下工作,传递彼此的状态,有的在等待、有的在执行、有的在验证。

你看这个 Verifier,像不像吹毛求疵的「甲方」?

最终整个任务交付的文件数量达到了惊人的 10 多个,包括 xls、ppt、html 网页,以及对应的 .md 版本。

▲ Agent Team 生成的财务预算表格,包括项目预算总表、现金流预测、票价和赞助定价模型,以及成本明细台账。

接下来再说一下这次 Mavis 的另一大特性:能连接到聊天平台,还支持多任务。

和 MiniMax 此前已经支持的 OpenClaw、Hermes Agent 类似,Mavis 本身也可以通过微信、飞书这两个 IM 管道来实现任务分配。接入流程也极度简化,只要点击设置按钮、扫码、命名,我们就能在微信/飞书里面使用 Mavis 了。

一般的 Agent 产品连接到 IM 当中里,我们给他安排一项需要长时间完成的任务,往往是消息发送之后,就不能再和他咨询别的问题。

一部分原因,在于这些 agent 时无法同时打开多个对话窗口;另一个原因则是 agent 工作模式的限制,在一个会话里运行多个任务,极易出现语境错乱的情况,导致上下文污染。

MiniMax 的解决方案,是把「秒回」和「执行」的逻辑解耦。

APPSO在飞书里让它研究一下最近石油涨价;任务开始之后,我又让它研究最近一个月硅谷 AI 巨头发布的重要产品。

Mavis 没有停止之前的任务,直接告诉我新任务已经完成了,而石油涨价的任务还在处理。

这正是 Mavis 的另一大设计理念:上下文隔离的好处。

每个 Agent Team,以及 team 里的每个 agent,都只看到跟自己任务相关的信息摘要,只有需要细节的时候才会去读全文。

这么做一来 token 成本受控,团队规模再大,上下文也不容易撑爆;二来防上下文污染,agent 在搜索中接触到的错误信息不会让全队阵亡。

在最极限的场景下,我们试过通过飞书在极短时间内给他分配 8 个任务,都没有发生语境错乱的情况。

整个体验,很像跟一个认知带宽极高的同事共事:不仅能秒回信息、同时后台干活也不会被打断。想了解一下进度,大可直接问,不用担心干扰它的「心流」。

处理不同会话的 Agent,只看到和自己任务相关的信息,不会共享一个不断膨胀的对话历史。

可以说,Mavis 实现了一个从 IM 渠道,到任务中枢,再到分子任务里的每个分子 agent——端到端的上下文隔离。

最后,它在解答 AI 大厂本月新发布和具身智能重要产品的同时,也顺利完成了石油任务这条主线程,给了我们一版详细的报告,里面甚至提到最近日本薯片包装要变成黑白的消息。

经过实测之后,你有没有发现,Mavis 这套编排策略,其实有点像此前火过一阵的「三省六部」skill?

每个角色做什么,何时启动、何时交接,将会由引擎层面的状态机来决定,而非模型的黑箱自己「拍脑门」说了算。

说白了,这就是在多 agent 工作编排当中,用工程层面的可控性、严密性、确定性,来根治模型的不可控、随机性。

这种思路,彻底解决了过去的 agent/模型「既当裁判又当选手」的经典问题。

额度统一,Agent 管够

实测 Mavis 之后,再说说 MiniMax 做的另一件同样重要的事情,影响所有的付费用户:这次,Token Plan 和 Agent Plan 合并了。

合并了之后,无论是普通用户的「日常使用」,比如官网上和 App 里对话和使用 Agent,还是接入官方 API 来调用其他工具(例如 coding 产品或 OpenClaw/Hermes Agent)——现在都可以使用统一的套餐额度了。并且,无论是 M2.7 以及后续的旗舰模型,还是音乐、视频、语音的多模态模型,全部包含在这一个套餐之下。

所有额度共享,怎么花用户可以自己说了算。MiniMax 还给出福利:此前同时订阅两个方案的用户,将会额外送一个月的会员。

为什么要做这件事?站在用户视角其实还是很合理的。

说白了,Agent 时代,用户付费动机来自于对「模型算力」的需求,而这些需求的场景随着模型在 coding、agent、多模态能力上的提升,只会变得愈发多元,会自然而然地发生在模型厂商的产品里(官网、独立产品、CLI)以及产品之外(接入外部 API 的独立部署的 agent)。

这其实也是各大 AI 巨头都在面对的问题:OpenAI 目前用户订阅和 API 计费还是分开的,Anthropic 同样;至于更小的 agent 创业公司,则是用自己的订阅费用去代替用户支付支付底层的 api 费用。

这一次,MiniMax 先一步把自己产品矩阵内部的墙拆掉了。而 APPSO 认为,在模型极度商品化、用户总是一窝蜂涌向最新、最便宜模型 API 的今天,这种统一套餐的策略,反而有助于为模型厂商维护用户忠诚度。

再回到产品本身。

如前所述,APPSO 正在写一篇关于「对 coding/agent 认真的模型厂商,必须要做自己的 coding/agent 产品」的文章。MiniMax 可以说是虽迟但到。

在今天,Mavis 也不是第一个押注多 agent 架构的产品。在过去半年里,ChatGPT、Manus、Genspark 等公司都参与到这场「多 agent」的战争当中。

而在实测跑完之后,APPSO 的感受是,Mavis 在「产品自己跑完一个极复杂/极长程任务」这件事上,做的比同行效果更好、架构也更稳定。当其它产品的多 agent 停留在提示词编排、拆任务上的时候,Mavis 做出了工程层面的对抗式硬约束——这带来的体感差异,足够明显。

不过,这套架构看起来美好,也有绕不开的现实:贵。

MiniMax 在技术博客中提出了多 agent 的「共识成本」(Cost of Consensus) 。用人话来说,几个 agent 彼此「制衡」,的确让工作过程和结果更靠谱,但取得共识的过程是有成本的,token 消耗数倍于单一 agent;而且就像吵架一样,吵急眼了也有可能偏离主题,准确率不升反降。

根据 MiniMax 梳理,其 Agent Team 架构具体来说有三类成本:

一是交接成本。信息在 agent 之间传递时需要重新组织,每次交接都要把信息「翻译」为下一个 agent 能用的形态,耗费 token;

二是共享(上下文信息的)成本。上下文隔离设计,一定程度上就是为了控制这一成本。但即便每个 agent 只看其他 agent 传递过来的「摘要」,随着 Agent Team 的量级扩大,存储和分发摘要都会带来成本。

三是聚合成本。其实这个道理,APPSO 一直很想跟大家讲:别以为那种成百上千个 skill、设计了极其复杂的「三省六部」制度的工作流就是卍解——很多时候并非如此,反而可能中了 token 厂商的计……你的确让工作变得更细致了,但你同时也需要花更多的 token去聚合和整理最终结果。

这些成本加起来,意味着多 agent 这件事从来不是「越多 agent 越好」的简单逻辑。

但换个角度看:信息交互越复杂的工作,往往本身价值就越高。一份需要多方核查、反复校验的深度研究报告,和一个随手问的问题,或许就不应该用同一套逻辑去衡量成本。Mavis 贵,贵在它认真,而认真处理的那些任务,本就值得这个价。

宁愿花更多成本去确保万无一失,也不愿意糊弄了事,这才是复杂任务背后的高价值用户所看重的。

当然,MiniMax 团队也做了一些工程设计去避免程序冗余带来的 token 浪费。

MiniMax 对用户的建议是:Agent Team 是为「贵且复杂」的任务准备的,是一个策略选项,而非默认选项。用户自行判断任务的复杂程度、链路长短、风险、经验复用的价值——这些越高,越值得用 Agent Team。反之,完全可以用单 agent,甚至普通的 chat。

多 Agent 一定多聪明吗?非也。但 Mavis 的意义,是让那些真正复杂、知识密集型的任务,不给模型自己拍脑门,而是交给一套经过验证的,有对抗、有核查、有权责划分和奖惩制度的工程系统。

它不一定让 AI 变得更聪明,但绝对会让 AI 更难偷懒——这也是大模型本身长期存在的老大难。

毕竟在真正的人际工作中,我们其实真的不需要同事多聪明……只是别偷懒,别耍小聪明,往往就够了,不是吗?

文|杜晨、张子豪

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

山东赫达:澄清实控人减持、质押及美国工厂等不实传闻

2026年5月13日 20:53
36氪获悉,山东赫达公告,公司关注到网络平台有关公司的不实传闻,包括实控人毕于成家族紧急减持、股权质押爆仓、资产注入终止、董事长被调查及美国工厂出问题等。经核实,公司实际控制人毕心德及其一致行动人毕于东、毕文娟合计持股39.75%,自上市以来除2021年、2019-2020年减持外无其他减持,目前无减持计划;其股票质押已于2023年6月全部解除,目前无质押;未披露或实施个人资产注入计划。截至本公告披露日,公司董事长毕于东正常履职,近期不存在接受行政机关或司法机关等公权力机关的调查、谈话或被要求协助调查、被采取留置等强制措施的异常情形。美国工厂项目正常推进,预计2026年投产。公司经营正常,无应披露未披露重大事项。

日本4月企业破产数达883家,同比增加6.6%

2026年5月13日 20:50
日本东京商工调查公司5月13日公布的日本全国4月破产企业数(负债额1000万日元以上)较上年同期增加6.6%至883家,连续5个月同比增加。人手不足及物价高涨使经营承压,负债总额增加8.8%至1118亿日元。(界面)

仁智股份:实控人筹划控制权变更事项,股票停牌

2026年5月13日 20:47
36氪获悉,仁智股份公告,公司收到控股股东、实际控制人陈泽虹通知,其正在筹划公司控制权变更相关事宜,可能导致公司控股股东、实际控制人发生变更。公司股票自2026年5月14日开市起停牌,预计停牌时间不超过2个交易日。本次控制权变更拟转让的股权比例为总股本的19.51%。

快克智能:先进封装TCB热压键合设备正与几家客户推进打样验证工作 未形成收入

2026年5月13日 20:45
36氪获悉,快克智能公告,公司股票连续3个交易日收盘价涨幅偏离值累计超20%,属于异常波动。公司主要产品包括精密焊接装联设备、机器视觉制程设备、智能制造成套装备和固晶键合封装设备。对于市场关注的相关设备情况,说明如下:2025年度,公司固晶键合封装设备业务营业收入占比不足5%,金额占比较小,其中碳化硅微纳银(铜)烧结设备收入占比极低,先进封装TCB热压键合设备正与几家客户推进打样验证工作,未形成收入,验证过程时间较长,未来能否打样成功及订单落地具有较大不确定性,对公司当期业绩无影响。相关业务后续存在不确定性,请投资者注意投资风险。

三七互娱:2026年第一季度拟每10股派现2.10元

2026年5月13日 20:41
36氪获悉,三七互娱公告,公司2026年第一季度利润分配预案为:以分红派息股权登记日实际发行在外的总股本剔除回购专用证券账户股份为基数,向全体股东每10股派送现金股利2.10元(含税),不送红股,不转增。按当前总股本2,212,237,681股扣减回购股份12,539,547股后的2,199,698,134股为基数测算,现金分红总额约4.62亿元(含税)。

截至4月30日累计有868款生成式人工智能服务完成备案

2026年5月13日 20:38
36氪获悉,据“网信中国”消息,2026年3月至4月新增72款生成式人工智能服务在国家网信办完成备案,对于通过API接口或其他方式直接调用已备案模型能力的生成式人工智能应用或功能,由地方网信办开展登记,新增49款完成登记。截至4月30日,累计有868款生成式人工智能服务完成备案,530款生成式人工智能应用或功能完成登记。

中国巨石:拟投资44.31亿元建设年产5万吨电子纱暨3.2亿米电子布生产线项目

2026年5月13日 20:30
36氪获悉,中国巨石公告,公司全资子公司巨石集团之全资子公司巨石淮安拟建设年产5万吨电子纱暨3.2亿米电子布生产线建设项目,总投资44.31亿元。项目建成后,预计总投资收益率10.81%,有利于公司调整产品结构、提升盈利能力和国际竞争力。

恩捷股份:终止购买中科华联100%股份事项

2026年5月13日 20:25
36氪获悉,恩捷股份公告,公司决定终止发行股份购买资产并募集配套资金事项。因交易各方对标的公司中科华联的估值认定未达成一致,未能就交易对价等核心条款达成共识。公司承诺自公告之日起1个月内不再筹划重大资产重组。终止本次交易不会对公司生产经营和财务状况造成重大不利影响。
❌
❌