<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ freeCodeCamp 中文编程教程：Python、JavaScript、Java、Git 等 ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ freeCodeCamp 中文编程教程：Python、JavaScript、Java、Git 等 ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 06 Oct 2026 06:06:33 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 Landlock 对 Linux 进程进行沙箱隔离，无需 root 权限 ]]>
                </title>
                <description>
                    <![CDATA[ 下面这个程序先限制自身，然后尝试读取两个文件： without landlock:   read /etc/hostname            ok   read /tmp/secret.txt          ok with landlock, /etc allowed:   read /etc/hostname          ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-sandbox-a-linux-process-with-landlock-no-root-required/</link>
                <guid isPermaLink="false">6aae5336d6857d046b7c6a1f</guid>
                
                    <category>
                        <![CDATA[ Linux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Thu, 17 Sep 2026 09:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2026/09/a2ed03fe-31d5-4b0b-af8b-88e6a7b54cac.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-sandbox-a-linux-process-with-landlock-no-root-required/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Sandbox a Linux Process with Landlock, No Root Required</a>
      </p><!--kg-card-begin: markdown--><p>下面这个程序先限制自身，然后尝试读取两个文件：</p>
<pre><code class="language-text">without landlock:
  read /etc/hostname            ok
  read /tmp/secret.txt          ok
with landlock, /etc allowed:
  read /etc/hostname            ok
  read /tmp/secret.txt          FAILED (Permission denied)
</code></pre>
<p>这里没有 root 权限，没有容器，没有配置文件，也没有守护进程。这个程序请求内核收回它自身对文件系统大部分区域的访问权限，而内核照做了。</p>
<p>这就是 Landlock。它自 2021 年起便已存在于内核中，却没有引起大多数人的注意。本文将从零开始构建这个程序并运行它，然后介绍四个初次使用 Landlock 时常让人意外的地方。</p>
<h2 id="">目录</h2>
<ul>
<li>所需条件</li>
<li>Landlock 所处的位置</li>
<li>三个系统调用，不需要库</li>
<li>一个限制自身的程序</li>
<li>为什么必须使用 <code>no_new_privs</code></li>
<li>规则集取交集，绝不会扩大权限</li>
<li>子进程会继承什么</li>
<li><code>exec</code> 陷阱</li>
<li>封装一个并非由你编写的程序</li>
<li>找出程序需要访问的路径</li>
<li>确定你拥有的 ABI 版本</li>
<li>结论</li>
<li>后记</li>
</ul>
<h2 id="">所需条件</h2>
<p>要跟着本文操作，你需要 5.13 或更高版本的内核、标准头文件以及一个 C 编译器。除此之外不需要任何东西，尤其不需要 root 权限。</p>
<pre><code class="language-bash">grep landlock /sys/kernel/security/lsm
ls /usr/include/linux/landlock.h
</code></pre>
<p>第一条命令很重要。Landlock 即使被编译进内核，也仍有可能处于未启用状态，因为 Linux 安全模块必须在启动时启用。在这台机器上，该文件的内容是 <code>lockdown,capability,landlock,yama,apparmor</code>。</p>
<p>如果你的输出中没有 <code>landlock</code>，请在内核命令行的现有列表前面添加 <code>lsm=landlock,</code>，然后重启。Ubuntu 从 22.04 版本开始便默认启用了它，当前的 Fedora 和 Arch 内核也包含它，但只有上面的 <code>grep</code> 命令才能给出适用于你这台机器的确切答案。</p>
<p>下文中的所有操作均在 Ubuntu 22.04.5、5.15.0-190-generic 内核上运行，使用 gcc 11.4 编译，整个过程都以普通用户身份完成，没有在任何地方使用 sudo。</p>
<h2 id="landlock">Landlock 所处的位置</h2>
<p>Linux 安全模块（Linux Security Modules，LSM）是一个框架，而不是一项策略。在打开文件、创建进程或映射可执行内存之前，内核会在这些决策点调用 LSM 钩子，而所有已加载的模块都可以决定允许还是拒绝相应操作。</p>
<p>SELinux 和 AppArmor 是大多数人听说过的两个安全模块，它们都是供管理员使用的工具：拥有 root 权限的人编写策略，系统加载该策略，而你的程序只能在策略规定的范围内运行。</p>
<p>Landlock 把这种方式反了过来。它是第一个允许进程在运行时无需特权便可对自身应用限制的 LSM。你不需要说服管理员为你的程序制定一项策略。程序请求减少自己当前拥有的权限，内核则相应收窄其权限。</p>
<p>这种“请求减少权限”的机制正是整个设计的核心。Landlock 只能移除访问权限。它没有任何调用能够授予你原本不具备的权限，而这恰恰是可以安全地让非特权进程使用它的原因。</p>
<h2 id="">三个系统调用，不需要库</h2>
<p>Landlock 由三个系统调用组成，glibc 没有为其中任何一个提供封装函数，因此你需要通过 <code>syscall()</code> 直接调用它们：</p>
<pre><code class="language-c">static int create_ruleset(const struct landlock_ruleset_attr *attr)
{ return syscall(__NR_landlock_create_ruleset, attr, sizeof(*attr), 0); }

static int add_rule(int fd, const struct landlock_path_beneath_attr *pb)
{ return syscall(__NR_landlock_add_rule, fd, LANDLOCK_RULE_PATH_BENEATH, pb, 0); }

static int restrict_self(int fd)
{ return syscall(__NR_landlock_restrict_self, fd, 0); }
</code></pre>
<p><code>landlock_create_ruleset</code> 声明你打算管控哪些访问类型，并返回一个代表该规则集的文件描述符。<code>landlock_add_rule</code> 以你希望保留访问权限的目录为形式添加一项例外。最后，<code>landlock_restrict_self</code> 将整个规则集永久应用于调用进程。</p>
<p>规则集属性中的 <code>handled_access_fs</code> 字段是人们经常理解反的部分。它列出的不是你要允许的操作，而是这个规则集负责处理的访问类型。凡是列入其中的访问类型，除了你明确添加的路径外，其他地方一律拒绝。只要把读取访问纳入处理范围，你就会失去对整个文件系统的读取权限，直到重新通过规则添加允许访问的路径。</p>
<h2 id="">一个限制自身的程序</h2>
<p>下面是完整程序：</p>
<pre><code class="language-c">#define _GNU_SOURCE
#include &lt;linux/landlock.h&gt;
#include &lt;sys/prctl.h&gt;
#include &lt;sys/syscall.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;errno.h&gt;

#define READ_RIGHTS (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR)

static int create_ruleset(const struct landlock_ruleset_attr *attr)
{ return syscall(__NR_landlock_create_ruleset, attr, sizeof(*attr), 0); }
static int add_rule(int fd, const struct landlock_path_beneath_attr *pb)
{ return syscall(__NR_landlock_add_rule, fd, LANDLOCK_RULE_PATH_BENEATH, pb, 0); }

static int restrict_self(int fd)
{ return syscall(__NR_landlock_restrict_self, fd, 0); }

static int allow_read(int ruleset_fd, const char *path)
{
    struct landlock_path_beneath_attr pb = { .allowed_access = READ_RIGHTS };
    int rc;

    pb.parent_fd = open(path, O_PATH | O_CLOEXEC);
    if (pb.parent_fd &lt; 0) { perror(path); return -1; }
    rc = add_rule(ruleset_fd, &amp;pb);
    close(pb.parent_fd);
    return rc;
}

static void try_read(const char *path)
{
    int fd = open(path, O_RDONLY);

    if (fd &lt; 0)
        printf("  read %-24s FAILED (%s)\n", path, strerror(errno));
    else
        { printf("  read %-24s ok\n", path); close(fd); }
}

int main(void)
{
    struct landlock_ruleset_attr attr = { .handled_access_fs = READ_RIGHTS };
    int ruleset_fd = create_ruleset(&amp;attr);

    if (ruleset_fd &lt; 0) { perror("landlock_create_ruleset"); return 1; }
    if (allow_read(ruleset_fd, "/etc") &lt; 0) return 1;

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("prctl"); return 1; }
    if (restrict_self(ruleset_fd)) { perror("landlock_restrict_self"); return 1; }
    close(ruleset_fd);
    printf("with landlock, /etc allowed:\n");
    try_read("/etc/hostname");
    try_read("/tmp/secret.txt");
    return 0;
}
</code></pre>
<p>编译并运行它：</p>
<pre><code class="language-bash">gcc -Wall -o sandbox sandbox.c
echo "hunter2" &gt; /tmp/secret.txt
./sandbox
</code></pre>
<pre><code class="language-text">with landlock, /etc allowed:
  read /etc/hostname            ok
  read /tmp/secret.txt          FAILED (Permission denied)
</code></pre>
<p>这里有两个细节很重要。规则通过一个已打开的文件描述符而非路径字符串来引用目录。该描述符使用 <code>O_PATH</code> 打开，因此即使没有对目录本身的读取权限，也能获得一个句柄。此外，<code>restrict_self</code> 会立即对调用进程生效，而且无法撤销。</p>
<h2 id="no_new_privs">为什么必须使用 <code>no_new_privs</code></h2>
<p>移除 <code>prctl</code> 调用后，程序便无法正常工作：</p>
<pre><code class="language-text">landlock_restrict_self -&gt; Operation not permitted
</code></pre>
<p>这就是 <code>EPERM</code>，而且是有意如此设计的。<code>PR_SET_NO_NEW_PRIVS</code> 告诉内核，这个进程及其后代永远不能通过 <code>execve</code> 获得新的特权。正是这一点阻止沙箱中的进程通过运行 setuid 二进制文件逃逸。</p>
<p>如果没有这项保证，受限进程便可以执行 <code>sudo</code> 或任何 setuid 程序，并越过刚刚应用的限制。Landlock 宁愿完全拒绝应用自身，也不会提供一个带有这种漏洞的沙箱。每次都要先设置 <code>no_new_privs</code>。</p>
<h2 id="">规则集取交集，绝不会扩大权限</h2>
<p>这个特性必须理解正确。先应用一个允许访问 <code>/etc</code> 的规则集，再应用第二个允许访问 <code>/tmp</code> 的规则集，然后看看你能访问哪些位置：</p>
<pre><code class="language-text">after first ruleset:  /etc=ok               /tmp=Permission denied
after second ruleset: /etc=Permission denied  /tmp=Permission denied
</code></pre>
<p>第二个规则集并没有添加 <code>/tmp</code>。它移除了对 <code>/etc</code> 的访问权限，让你最终什么也无法访问。</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6a783a81a29db580b40f1bc8/3091ccb0-3d7e-4b52-8744-83a7b32848b9.png" alt="Landlock 沙箱分三个阶段逐步收紧：没有规则集时，五个目录全部可读；应用仅允许访问 /etc 的规则集后，只有 /etc 可读；再应用允许访问 /tmp 的第二个规则集后，由于两个规则集取交集且交集为空，所有目录都不可读" width="600" height="400" loading="lazy"></p>
<p>每次调用 <code>restrict_self</code>，都会与此前已应用的所有规则取交集。第一个规则集允许访问 <code>/etc</code> 并拒绝其余路径；第二个规则集允许访问 <code>/tmp</code> 并拒绝其余路径。最终保留下来的是两者重叠的部分，而这个交集为空。</p>
<p>因此，Landlock 沙箱就像一个棘轮。每次应用都只能进一步收紧限制，绝不能放宽，而且整个 API 中不存在任何能够扩大受限进程访问权限的操作。如果你需要一个进程访问两个目录，就必须在应用规则集之前，把两条规则都放入同一个规则集。</p>
<p>这也意味着你不能反悔。一个长时间运行的进程如果很早就限制了自身，那么此后无论是它自己还是其他任何主体，都无法再向它授予更多权限，除非启动一个新进程。</p>
<h2 id="">子进程会继承什么</h2>
<p>限制会自动随 <code>fork</code> 继承：</p>
<pre><code class="language-text">parent:       /etc=ok   /tmp/secret.txt=Permission denied
forked child: /etc=ok   /tmp/secret.txt=Permission denied
</code></pre>
<p>子进程会完整继承父进程的 Landlock 域，而且没有任何标志可以选择退出。跨越 <code>execve</code> 时也是如此，而这正是 <code>no_new_privs</code> 的意义所在：新程序一开始就已经处于旧程序建立的沙箱中。</p>
<p>这使得 Landlock 可用于封装并非由你编写的程序。先限制自身，再执行你想要约束的程序，它便会在你的限制范围内运行，根本不需要知道这些限制的存在。</p>
<h2 id="exec"><code>exec</code> 陷阱</h2>
<p>这也设置了一个陷阱。沿用上面的程序，只允许访问 <code>/etc</code>，然后尝试执行任何程序：</p>
<pre><code class="language-text">allowed: /etc
execl(/usr/bin/cat) failed: Permission denied
</code></pre>
<p>执行二进制文件需要读取它。这个规则集处理 <code>LANDLOCK_ACCESS_FS_READ_FILE</code>，因此内核会检查是否允许读取 <code>/usr/bin/cat</code>。由于找不到涵盖 <code>/usr</code> 的规则，内核会在程序真正启动之前拒绝执行。</p>
<p>把 <code>/usr</code> 添加到同一个规则集后，它就能正常工作：</p>
<pre><code class="language-text">allowed: /etc and /usr
devils-dell
</code></pre>
<p>一般原则是，Landlock 沙箱必须包含进程接触的所有内容，而这个集合比你想象的更大。其中包括你的二进制文件、它的解释器、它加载的每一个共享库，以及它在启动时读取的所有配置。对二进制文件运行 <code>ldd</code>，是着手整理这份列表的一个好方法。</p>
<h2 id="">封装一个并非由你编写的程序</h2>
<p>跨越 <code>exec</code> 的继承机制，使 Landlock 不仅能用于你自己的代码。先限制自身，再执行任何你想要约束的程序，它便会在沙箱中运行，无需配合，甚至根本不知道沙箱的存在。</p>
<p>要把上面的程序变成一个可用的封装器，需要增加一项内容。除了读取权限外，还要处理 <code>LANDLOCK_ACCESS_FS_EXECUTE</code>；允许访问任何二进制程序都需要的系统目录；然后允许访问用户指定的工作目录：</p>
<pre><code class="language-c">#define RIGHTS (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | \
                LANDLOCK_ACCESS_FS_EXECUTE)

static int add_path(int ruleset_fd, const char *path)
{
    struct landlock_path_beneath_attr pb = { .allowed_access = RIGHTS };
    int rc;

    pb.parent_fd = open(path, O_PATH | O_CLOEXEC);
    if (pb.parent_fd &lt; 0)
        return -1;
    rc = syscall(__NR_landlock_add_rule, ruleset_fd,
                 LANDLOCK_RULE_PATH_BENEATH, &amp;pb, 0);
    close(pb.parent_fd);
    return rc;
}

int main(int argc, char **argv)
{
    struct landlock_ruleset_attr attr = { .handled_access_fs = RIGHTS };
    const char *base[] = { "/usr", "/lib", "/lib64", "/bin", "/etc" };
    int fd, i;

    if (argc &lt; 3) { fprintf(stderr, "usage: %s DIR CMD...\n", argv[0]); return 2; }

    fd = syscall(__NR_landlock_create_ruleset, &amp;attr, sizeof(attr), 0);
    if (fd &lt; 0) { perror("create_ruleset"); return 1; }

    for (i = 0; i &lt; (int)(sizeof(base) / sizeof(*base)); i++)
        add_path(fd, base[i]);
    if (add_path(fd, argv[1]) &lt; 0) { perror(argv[1]); return 1; }
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("prctl"); return 1; }
    if (syscall(__NR_landlock_restrict_self, fd, 0)) { perror("restrict_self"); return 1; }
    close(fd);

    execvp(argv[2], &amp;argv[2]);
    fprintf(stderr, "%s: %s\n", argv[2], strerror(errno));
    return 1;
}
</code></pre>
<p>循环忽略了 <code>add_path</code> 的返回值，因此同一个二进制程序可以在不存在 <code>/lib64</code> 或 <code>/bin</code>，或者这些路径是指向其他位置的符号链接的系统上运行。用户指定的目录则会接受检查，因为其中若有拼写错误，就应该立即报错，而不是稍后出现令人困惑的拒绝访问错误。</p>
<p>现在，<code>cat</code> 和 <code>ls</code> 只能看到工作目录，无法看到其他任何位置：</p>
<pre><code class="language-bash">mkdir -p /tmp/work &amp;&amp; echo "project data" &gt; /tmp/work/notes.txt

./llrun /tmp/work cat /tmp/work/notes.txt
./llrun /tmp/work cat /tmp/secret.txt
./llrun /tmp/work ls /home
</code></pre>
<pre><code class="language-text">project data
cat: /tmp/secret.txt: Permission denied
ls: cannot open directory '/home': Permission denied
</code></pre>
<p>这两个程序都没有经过修改、重新编译，也没有被要求表示同意。<code>bwrap</code> 和类似工具通过在程序周围构建挂载命名空间和用户命名空间来达到这种效果。而这里通过请求一个 LSM 减少权限便实现了同样的结果，代码大约只有 60 行，而且无需安装任何东西。</p>
<h2 id="">找出程序需要访问的路径</h2>
<p>任何沙箱最困难的部分都不是 API，而是那份路径列表。程序打开的内容远比你预想的多，漏掉的某个路径会在运行过程的深处以失败的形式暴露出来。</p>
<p>有两个工具可以帮你建立这份列表。<code>ldd</code> 会给出共享库，而程序必须能够读取它们，否则根本无法启动：</p>
<pre><code class="language-bash">ldd /usr/bin/cat
</code></pre>
<pre><code class="language-text">linux-vdso.so.1 (0x00007fff85f39000)
libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4f61bb5000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4f61e0a000)
</code></pre>
<p><code>strace</code> 会给出其余所有内容。先在不受限制的情况下运行程序，并收集它打开的路径：</p>
<pre><code class="language-bash">strace -e trace=openat cat /etc/hostname 2&gt;&amp;1 | grep -oE '"/[^"]+"' | sort -u
</code></pre>
<pre><code class="language-text">"/etc/hostname"
"/etc/ld.so.cache"
"/lib/x86_64-linux-gnu/libc.so.6"
"/usr/lib/locale/locale-archive"
</code></pre>
<p>一个只打印单个文件的程序却访问了四条路径，而且其中只有一条是你要求它读取的文件。链接器缓存、C 库和区域设置归档全都必不可少，因此封装器会先允许访问 <code>/usr</code>、<code>/lib</code> 和 <code>/etc</code>，然后才允许访问你选择的任何路径。</p>
<p>应用限制后，<code>strace</code> 也能准确告诉你哪项操作遭到了拒绝：</p>
<pre><code class="language-bash">strace -f -e trace=openat ./llrun /tmp/work cat /tmp/secret.txt 2&gt;&amp;1 | grep EACCES
</code></pre>
<pre><code class="language-text">openat(AT_FDCWD, "/tmp/secret.txt", O_RDONLY) = -1 EACCES (Permission denied)
</code></pre>
<p>这就是整个迭代过程：运行程序，查看包含 <code>EACCES</code> 的行，判断相应路径究竟应该加入规则集，还是程序本就不该尝试访问它，然后重复操作。在这台机器所用的内核上，Landlock 自身不记录任何日志，因此 <code>strace</code> 就是调试器。从 6.15 开始的内核会通过审计子系统报告拒绝事件，所以在寻找一个并不存在的日志之前，请先检查你的内核版本。</p>
<h2 id="abi">确定你拥有的 ABI 版本</h2>
<p>Landlock 自 5.13 版本以来一直在发展，而你读到的功能可能并不存在于自己的内核中。直接询问内核即可：</p>
<pre><code class="language-c">int v = syscall(__NR_landlock_create_ruleset, NULL, 0,
                LANDLOCK_CREATE_RULESET_VERSION);
</code></pre>
<p>这台机器返回 <code>1</code>，也就是 5.13 中最初提供的版本，其中包含 13 种文件系统访问权限：</p>
<pre><code class="language-bash">grep -oE "LANDLOCK_ACCESS_FS_[A-Z_]+" /usr/include/linux/landlock.h | sort -u
</code></pre>
<pre><code class="language-text">LANDLOCK_ACCESS_FS_EXECUTE      LANDLOCK_ACCESS_FS_MAKE_BLOCK
LANDLOCK_ACCESS_FS_MAKE_CHAR    LANDLOCK_ACCESS_FS_MAKE_DIR
LANDLOCK_ACCESS_FS_MAKE_FIFO    LANDLOCK_ACCESS_FS_MAKE_REG
LANDLOCK_ACCESS_FS_MAKE_SOCK    LANDLOCK_ACCESS_FS_MAKE_SYM
LANDLOCK_ACCESS_FS_READ_DIR     LANDLOCK_ACCESS_FS_READ_FILE
LANDLOCK_ACCESS_FS_REMOVE_DIR   LANDLOCK_ACCESS_FS_REMOVE_FILE
LANDLOCK_ACCESS_FS_WRITE_FILE
</code></pre>
<p>后续版本增加了文件重新归属、截断操作、涵盖 TCP 绑定和连接的网络规则，以及对设备 <code>ioctl</code> 的控制。每项功能都伴随一次独立的 ABI 版本升级，因此，需要使用较新权限的程序应该查询 ABI 版本并进行降级处理，而不能想当然。传入运行中内核无法识别的权限，会使 <code>landlock_create_ruleset</code> 以 <code>EINVAL</code> 失败。如果你没有先检查版本，这会成为一个令人困惑、难以调试的错误。</p>
<h2 id="">结论</h2>
<p>现在，你已经可以从进程内部对其自身实施沙箱隔离，无需特权，也无需配置，并且了解了四个出人意料的地方。必须先设置 <code>no_new_privs</code>，否则不会应用任何限制。规则集会取交集，而不是不断累加权限，因此应当构建一个包含全部所需内容的规则集。子进程会继承限制，这是一个特性。最后，读取限制会破坏 <code>exec</code>，除非二进制文件所在的路径也被允许访问。</p>
<p>接下来可以从几个方向继续探索。把 <code>LANDLOCK_ACCESS_FS_WRITE_FILE</code> 加入 <code>handled_access_fs</code>，构建一个可以广泛读取、却只能写入一个指定目录的程序。先限制自身，再调用 <code>execve</code>，从而封装一个并非由你编写的程序。或者观察 <code>strace</code> 如何报告拒绝事件，这是建立真实程序实际所需路径列表的最快方法。</p>
<h2 id="">后记</h2>
<p>Landlock 最值得探讨的问题并不是它能做什么，而是它无法表达什么。规则集指定的是路径，因此权限的基本单位是文件系统中的位置，而不是交给你的某个特定文件。你可以规定这个进程可以读取 <code>/etc</code> 下的内容，却无法规定这个进程只能读取用户刚刚选中的那一个文件，其他内容一概不能读取。</p>
<p>过去一段时间里，我一直在尝试弥补这一缺口，构建一个以 capability 机制为基础的桌面操作系统。在这个系统中，权限通过指向具体对象的句柄来授予，而不是通过针对某个路径或位置制定规则，同时底层仍然保持 Debian 生态系统正常运行。Landlock 承担了其中很大一部分工作，而它能力所及的边界之外，恰恰是系统设计开始变得有意思的地方。</p>
<p>我在 <a href="https://thechris.in/">thechris.in</a> 撰写有关系统及其奥秘的文章。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何对 Go 应用进行 Docker 容器化：完整分步指南 ]]>
                </title>
                <description>
                    <![CDATA[ 假设你想把源代码分享给一个电脑上没有安装 Go 的人。遗憾的是，这个人将无法运行你的应用。即使对方安装了 Go，由于你们的本地开发环境不同，应用的行为也可能有所差异。 那么，怎样才能把应用及其所需内容打包起来，让它在任何本地环境中都以相同的方式运行呢？这正是 Docker 的用武之地。 对于初学者来说，Docker 并不总是一个容易理解的概念。但一旦理解了它，我敢保证，你会觉得它非常有趣，甚至想把能接触到的每个应用都进行 Docker 容器化。 本文将以一个 Go 应用为例。这里讲解的容器化基本概念可以迁移到其他场景，因此不必过于担心使用其他语言开发的应用该如何进行 Docker 容器化。 我们将介绍如何仅使用 Docker、镜像和容器对 Go 应用进行容器化，如何使用 Docker Compose 在一个应用中设置多个容器，以及 Docker Compose 文件由哪些部分组成。 读完本文后，你将对 Docker、镜像和容器有基本的了解，并知道如何使用 Docker Compose 编排多个相互依赖的容器。 本文内容  1.  前置条件  2.  什么是 Docker   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-dockerize-a-go-application-full-step-by-step-walkthrough/</link>
                <guid isPermaLink="false">6aadef8ed6857d046b7c6a0c</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Wed, 16 Sep 2026 02:17:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2026/09/e668b887-5ebf-4bd5-9eb6-1187fd650eb5.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-dockerize-a-go-application-full-step-by-step-walkthrough/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Dockerize a Go Application – Full Step-by-Step Walkthrough</a>
      </p><!--kg-card-begin: markdown--><p>假设你想把源代码分享给一个电脑上没有安装 Go 的人。遗憾的是，这个人将无法运行你的应用。即使对方安装了 Go，由于你们的本地开发环境不同，应用的行为也可能有所差异。</p>
<p>那么，怎样才能把应用及其所需内容打包起来，让它在任何本地环境中都以相同的方式运行呢？这正是 Docker 的用武之地。</p>
<p>对于初学者来说，Docker 并不总是一个容易理解的概念。但一旦理解了它，我敢保证，你会觉得它非常有趣，甚至想把能接触到的每个应用都进行 Docker 容器化。</p>
<p>本文将以一个 Go 应用为例。这里讲解的容器化基本概念可以迁移到其他场景，因此不必过于担心使用其他语言开发的应用该如何进行 Docker 容器化。</p>
<p>我们将介绍如何仅使用 Docker、镜像和容器对 Go 应用进行容器化，如何使用 Docker Compose 在一个应用中设置多个容器，以及 Docker Compose 文件由哪些部分组成。</p>
<p>读完本文后，你将对 Docker、镜像和容器有基本的了解，并知道如何使用 Docker Compose 编排多个相互依赖的容器。</p>
<h2 id="">本文内容</h2>
<ol>
<li>前置条件</li>
<li>什么是 Docker</li>
<li>如何安装 Docker</li>
<li>什么是 Dockerfile</li>
<li>什么是 Docker Compose</li>
<li><code>app</code> 容器</li>
<li><code>database</code> 容器</li>
<li><code>phpMyAdmin</code> 容器</li>
<li>一起运行所有服务</li>
<li>总结</li>
</ol>
<h2 id="">前置条件</h2>
<p>阅读本教程不需要具备任何 Docker 基础知识。本文从初学者的角度编写，所以即使你从未接触过这个概念也没有关系。</p>
<p>如果你具备一些 Go 基础知识，就能更好地理解并跟随本文中的 Go 代码示例。如果你已经知道如何在本地电脑上搭建 Go 应用环境，就可以继续阅读。如果还不知道，可以查看这篇关于<a href="https://www.freecodecamp.org/news/how-to-get-started-coding-in-golang/">如何开始使用 Go 编程</a>的文章。</p>
<h2 id="docker">什么是 Docker</h2>
<p>想象一下，你有一个盒子。你把代码以及代码运行所需的一切都放进这个盒子，包括代码使用的编程语言，以及需要安装的所有其他外部软件包。</p>
<p>如果有人需要你的应用，你只需把这个盒子交给对方。你也可以把它交给任意数量的人。他们不需要在自己的电脑上安装相应的编程语言或其他任何东西，因为所需的一切都已经装在盒子里。因此，他们运行应用时，实际上是在运行这个盒子的一个实例。</p>
<p>应用在盒子这个标准化环境中运行。这意味着，每个拿到并“打开”盒子的人，都会以完全相同的方式运行该应用。</p>
<p>借助 Docker，应用可以在不同系统上的一致环境中运行，从而避免“在我的电脑上明明能运行”的问题。</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/3b2b169d-d882-48a8-88bf-233e4acec611.png" alt="一个装有依赖项、运行时和源代码的盒子，通过箭头连接到多位开发者" width="800" height="332" loading="lazy"></p>
<p>用 Docker 的专业术语来说，这个盒子叫作<strong>镜像</strong>，镜像的运行实例叫作<strong>容器</strong>。</p>
<p>镜像是一种轻量级、独立且可执行的软件包，其中包含运行某个软件所需的一切，包括代码、运行时、库、系统工具，甚至操作系统环境。</p>
<p>容器就是镜像的一个可运行实例。它代表某个特定应用的执行环境。</p>
<p>如果这些内容听起来仍然很抽象，也不用担心。我们很快就会开始实际操作。</p>
<h2 id="docker">如何安装 Docker</h2>
<p>为了安装 Docker，我们将安装自带 Docker Engine 的 Docker Desktop。Docker Desktop 是一个用于管理容器的图形用户界面，你会在后面的章节中看到它有多实用。</p>
<p>在撰写本文时，我使用的是 WSL（Windows Subsystem for Linux，适用于 Linux 的 Windows 子系统）。如果你也使用 WSL，安装前需要考虑这一点，因为不同操作系统安装 Docker 的前置条件和步骤并不相同。</p>
<p>要在 WSL 环境中安装 Docker Desktop，请执行以下步骤：</p>
<ol>
<li>下载并安装 Windows 版 <a href="https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe"><code>.exe</code> 文件</a>。</li>
<li>从“开始”菜单启动 Docker Desktop，然后进入设置。</li>
<li>在 <strong>General</strong> 选项卡中选择 <strong>Use WSL 2 based engine</strong>。</li>
<li>单击 <strong>Apply</strong>。</li>
</ol>
<p>这样就完成了 WSL 环境中的安装。如果你使用其他操作系统，可以在<a href="https://docs.docker.com/get-started/introduction/get-docker-desktop/">官方文档</a>中查看相应的安装方式。</p>
<h2 id="dockerfile">什么是 Dockerfile</h2>
<p>为了创建前面提到的盒子，Docker 首先需要按照一系列明确的步骤操作。它需要知道应用的依赖项和运行时，还需要获得源代码。我们会把所有这些步骤都写在 Dockerfile 中。</p>
<p>在正式开始之前，先创建一个工作目录并进入该目录：</p>
<pre><code class="language-bash">mkdir go_book_api &amp;&amp; cd go_book_api
</code></pre>
<p>运行以下命令，在应用中初始化 Go 模块：</p>
<pre><code class="language-bash">go mod init go_book_api
</code></pre>
<p>执行这条命令需要本地已经安装 Go；但在项目文件准备完成后，其他人只需安装 Docker 就能构建和运行该项目。这条命令会创建一个 <code>go.mod</code> 文件，用来记录项目依赖项。在项目根目录下创建一个 <code>cmd</code> 目录，并在其中创建 <code>main.go</code> 文件。这个文件将作为应用的入口点。你可以在 <code>main.go</code> 文件中加入一条简单的输出语句：</p>
<pre><code class="language-go">// cmd/main.go
package main

import "fmt"

func main() {
	fmt.Println("Look at me gooo!")
}
</code></pre>
<p>接下来，在项目根目录下创建一个名为 <code>Dockerfile</code> 的文件。这个文件没有扩展名；默认情况下，<code>docker build</code> 会在构建上下文的根目录中查找这个名称的构建文件。</p>
<p>把以下内容粘贴到该文件中，然后我们逐项进行说明：</p>
<pre><code class="language-dockerfile"># base image
FROM golang:1.24

# define the working directory
WORKDIR /app

# copy go.mod so that the required modules are known in the container
# ./ here is the WORKDIR, /app
COPY go.mod ./

# command to download modules
RUN go mod download

# copy source code into working dir
COPY . .

# build
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping ./cmd/main.go

# run the compiled binary when the container starts
CMD ["/docker-gs-ping"]
</code></pre>
<p>大多数 Dockerfile 都以基础镜像开头，并使用 <code>FROM</code> 关键字指定基础镜像。基础镜像是一个底层模板，它提供在容器中构建和运行应用所需的精简操作系统环境、库或依赖项。</p>
<p>在本例中，基础镜像是 <code>golang:1.24</code>。基础镜像也可以提供 Linux 之类的操作系统环境。在这种情况下，即使你把代码交给没有使用 Linux 的人，对方也不必担心，因为应用将在已经包含精简 Linux 系统的容器环境中运行。同理，即使对方的本地电脑没有安装 Go，也能运行你的应用。</p>
<p>在编写 Dockerfile 时，如果不确定该使用哪种基础镜像，可以随时在 Docker Hub 中浏览已经发布的镜像。本例使用的是 <a href="https://hub.docker.com/_/golang">Docker 官方 Go 镜像</a>。</p>
<p>下一步是定义工作目录。容器内有一个类似 Linux 的文件系统，其中包含 <code>/app</code>、<code>/bin</code>、<code>/usr</code> 和 <code>/var</code> 等目录。本例通过 <code>WORKDIR</code> 指令把工作目录定义为 <code>/app</code>。</p>
<p>设置工作目录后，需要把 <code>go.mod</code> 文件复制到其中，以便 Docker 知道应用需要哪些模块。如果项目中已有 <code>go.sum</code>，通常也应将它一并复制，例如将相应指令写成 <code>COPY go.mod go.sum ./</code>。</p>
<p>Docker 的 <code>COPY</code> 指令至少接收两个参数：一个或多个源路径，以及一个目标路径。本例中的 <code>COPY go.mod ./</code> 会把 <code>go.mod</code> 复制到容器的工作目录 <code>/app</code> 中。</p>
<p>接下来，在容器环境中运行一条命令，把 <code>go.mod</code> 中定义的模块下载到模块缓存。要在构建镜像时运行命令，可以使用 <code>RUN</code>，后面接要执行的命令。本例执行的是 <code>go mod download</code>。</p>
<p>下一步是把所有源代码复制到工作目录中。</p>
<p>现在，依赖项和源代码都已经准备好了。最后，需要把 Go 应用构建为一个可在该环境，也就是容器内部运行的可执行文件。</p>
<p>容器中会生成一个位于 <code>/docker-gs-ping</code> 的已编译二进制文件，它由 <code>main.go</code> 中的代码编译而来。最后的 <code>CMD</code> 指令会告诉 Docker 在容器启动时运行这个可执行二进制文件。换句话说，就是“容器开始运行后，执行这个二进制文件”。</p>
<p>完成这些步骤后，Docker 会构建一个可以运行的镜像，也就是我们在类比中所说的盒子。要构建镜像，可以在终端中运行以下命令：</p>
<pre><code class="language-bash">docker build -t go_book_api .
</code></pre>
<p><code>docker build</code> 命令让 Docker 按照 Dockerfile 中的步骤构建镜像。<code>-t</code> 用于设置镜像标识，其格式可以包含镜像名称和标签。</p>
<p>本例将镜像命名为 <code>go_book_api</code>；由于没有显式指定标签，Docker 会使用默认标签 <code>latest</code>。命令末尾的 <code>.</code> 表示当前目录是构建上下文，Docker 会在其中查找 Dockerfile 以及需要复制到镜像中的文件。</p>
<p>下面是我在集成开发环境中构建镜像时的画面：</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/361a805e-153d-4034-9d9a-d34c9015738a.png" alt="集成开发环境的终端正在构建 Docker 镜像" width="1910" height="992" loading="lazy"></p>
<p>如果查看 Docker Desktop 的 <strong>Images</strong> 选项卡，就会看到镜像已经构建完成：</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/b569277e-295b-4a3d-8e51-fb91dd7e3d91.png" alt="Docker Desktop 中已经构建好的容器镜像" width="1881" height="363" loading="lazy"></p>
<p>你可以把这个镜像托管在 <a href="https://www.docker.com/products/docker-hub/">Docker Hub</a> 之类的公共镜像仓库平台上，并与朋友分享。他们可以拉取镜像，完成设置并运行你的应用，即使本地没有安装 Go 也没关系。他们只需要把容器运行起来。</p>
<p>单击最右侧的小型播放按钮，就可以从该镜像启动一个实例，也就是一个容器。</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/09726294-be22-458d-b660-5f6d32102205.png" alt="用于运行新容器的 Docker Desktop 对话框" width="825" height="758" loading="lazy"></p>
<p>你可以为容器指定一个描述性名称。如果不指定，Docker 会随机生成一个名称。然后单击 <strong>Run</strong> 按钮。容器开始运行后，页面会跳转到它的日志页面。</p>
<p>现在容器已经启动并执行了应用。由于当前程序只输出一行文字后就会退出，容器也会很快停止；你仍然可以在日志中看到程序的输出。</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/3133c16c-0950-4f03-9502-ae6495535c13.png" alt="Docker Desktop 中正在运行的 Docker 容器" width="1918" height="663" loading="lazy"></p>
<h2 id="dockercompose">什么是 Docker Compose</h2>
<p>如果你构建的是一个不需要任何外部依赖项的简单 Go 应用，那么上面的设置已经完全足够。</p>
<p>但在本例中，这个应用是一个图书 API，因此通常还需要数据库之类的服务，以及 phpMyAdmin 这样的数据库管理客户端，以便查看数据库表。</p>
<p>如果只使用一个 Dockerfile，把这些服务全部放进同一个容器会比较复杂。Dockerfile 虽然可以通过多阶段构建使用多个 <code>FROM</code> 指令，但 Go 应用、数据库和数据库管理工具是不同的长期运行服务，通常不应打包到同一个容器中。</p>
<p>你可以使用一个精简操作系统的基础镜像，再通过命令手动安装其他服务，但这会把多个组件紧密耦合在一起，使应用更难维护和扩展。某个组件出现故障时，也可能影响整个应用。</p>
<p>为了解决这个问题，Docker Compose 允许你为一个应用定义多个相互连接的服务。它可以协调容器的启动顺序和网络连接，并通过卷持久化或共享数据。</p>
<p>仍然沿用前面的盒子类比，只不过使用 Docker Compose 时，我们不再只有一个盒子：</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/2c890de4-8d5d-4457-a27a-fc441f58d794.png" alt="一个装有多个容器的盒子，通过箭头连接到不同的开发者" width="823" height="609" loading="lazy"></p>
<p>Docker Compose 的作用是帮助你编排运行应用所需的多项服务及其容器。可以把它理解为把多个盒子连接在一起。</p>
<p>按照前面的说明，Go 应用将在 <code>app</code> 服务的 <code>go_book_api</code> 容器中运行。把它扩展为实际的图书 API 后，可以由 <code>database</code> 服务提供 MySQL，并把图书数据保存在 <code>mysql-go</code> 命名卷中；你还可以通过 <code>phpmyadmin</code> 服务查看数据库。本文中的最小 Go 示例尚未实现数据库连接。</p>
<p>为了具体了解这种配置，请在项目根目录下创建一个 <code>docker-compose.yml</code> 文件。Docker Compose 默认查找 <code>compose.yaml</code>（首选）、<code>compose.yml</code>、<code>docker-compose.yaml</code> 或 <code>docker-compose.yml</code>。扩展名表明其中的配置使用 YAML 编写。YAML 是一种主要用于配置文件的数据序列化语言。</p>
<pre><code class="language-yaml">services:
  app:
    depends_on:
      - database
    build:
      context: .
    container_name: go_book_api
    networks:
      - go_book_api_net
    ports:
      - 8080:8080

  database:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USER}
    volumes:
      - mysql-go:/var/lib/mysql
    ports:
      - 3356:3306
    networks:
      - go_book_api_net

  phpmyadmin:
    image: phpmyadmin
    restart: always
    ports:
      - 9000:80
    environment:
      PMA_HOST: database
      PMA_ARBITRARY: 1
    depends_on:
      - database
    networks:
      - go_book_api_net

volumes:
  mysql-go:

networks:
  go_book_api_net:
    driver: bridge
</code></pre>
<p>Docker Compose 文件的顶层有一个 <code>services</code> 字段，其中定义了应用运行所需的所有服务。服务是抽象定义，每项服务可以由一个或多个容器实例实现。</p>
<h3 id="app"><code>app</code> 容器</h3>
<pre><code class="language-yaml">app:
  depends_on:
    - database
  build:
    context: .
  container_name: go_book_api
  networks:
    - go_book_api_net
  ports:
    - 8080:8080
</code></pre>
<p>第一项服务是 <code>app</code>，也就是你的 Go 应用。你还需要在 <code>app</code> 服务下定义它运行所需的一些参数。</p>
<p><code>depends_on</code> 属性控制服务的启动和停止顺序。这样一来，如果服务 A 依赖服务 B，系统就会先启动服务 B，再启动服务 A。在本例中，必须先启动 <code>database</code> 服务，再启动 <code>app</code> 服务。请注意，这并不意味着 <code>app</code> 一定会等到 <code>database</code> 已经可以接受请求后才启动。</p>
<p>下一个属性 <code>build</code> 告诉 Docker Compose 使用本地项目构建 Docker 镜像。由于应用的 Dockerfile 位于项目根目录，因此使用 <code>context</code> 属性并将根路径指定为 <code>.</code>。</p>
<p>要为容器指定名称，可以使用 <code>container_name</code>。在 Compose 网络中，服务之间通常使用服务名或网络别名进行通信；本例中，其他服务可以使用 <code>app</code> 引用这项服务。</p>
<p>Docker Compose 的目的在于让多个容器协同工作，而容器借助网络进行通信。如果没有显式声明网络，Compose 会为项目创建默认网络。本例显式定义了一个名为 <code>go_book_api_net</code> 的网络，并让需要相互通信的服务都加入这个网络。</p>
<p>下一个属性是 <code>ports</code>。后面我们会让这个 Go 应用启动一个 HTTP 服务器。要从本地访问它，需要把主机端口映射到容器端口。这里把电脑上的 <code>8080</code> 端口映射到容器中的 <code>8080</code> 端口。</p>
<p>Docker Compose 会读取项目目录中的 <code>.env</code> 文件，用其中的值替换 Compose 文件里的 <code>${DB_NAME}</code> 等表达式。</p>
<p>为了让本例中的数据库服务能够启动，<code>.env</code> 至少需要提供以下变量。下面的值仅用于演示，请换成适合本地开发环境的值：</p>
<pre><code class="language-dotenv">DB_ROOT_PASSWORD=change-this-root-password
DB_NAME=go_book_api
DB_USER=go_book_user
DB_PASSWORD=change-this-user-password
</code></pre>
<p>由于 Dockerfile 使用了 <code>COPY . .</code>，还应在项目根目录下创建 <code>.dockerignore</code>，防止 <code>.env</code> 被复制进镜像：</p>
<pre><code class="language-gitignore">.env
.git
</code></pre>
<p>同时应把 <code>.env</code> 加入 <code>.gitignore</code>，不要把真实凭据提交到版本库。</p>
<h3 id="database"><code>database</code> 容器</h3>
<pre><code class="language-yaml">database:
  image: mysql:8.0
  environment:
    MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    MYSQL_DATABASE: ${DB_NAME}
    MYSQL_PASSWORD: ${DB_PASSWORD}
    MYSQL_USER: ${DB_USER}
  volumes:
    - mysql-go:/var/lib/mysql
  ports:
    - 3356:3306
  networks:
    - go_book_api_net
</code></pre>
<p>第二项服务是 <code>database</code>。你可以自行命名列出的服务，不过通常应使用能够说明用途的服务名。</p>
<p>为了演示多容器配置，本例添加了 MySQL 数据库服务。当前的最小 Go 代码还没有连接数据库；实际扩展图书 API 时，可以通过这项服务读写数据。</p>
<p>请记住，创建容器需要镜像。在本例中，<code>image</code> 属性指定了现成的官方镜像 <code>mysql:8.0</code>。启动这项服务时，Docker Compose 会使用该镜像创建数据库容器。</p>
<p>如果你曾在本地配置过数据库，就会知道配置是一个无法省略的步骤。创建数据库需要设置用户、密码和数据库名称。可以在 <code>environment</code> 属性中引用这些变量，并在项目的 <code>.env</code> 文件或 shell 环境中提供实际值。不要把真实凭据直接写入 Compose 文件，也不要把包含真实凭据的 <code>.env</code> 文件提交到版本库。</p>
<p>无论数据库运行在本地还是远程，数据库服务器通常都会监听特定端口，以接收传入的连接。和 <code>app</code> 容器一样，可以为数据库设置一个本地端口，并把它映射到容器中的对应端口。如果要从本地访问数据库，应使用 <code>3356</code> 端口，连接流量会被转发到数据库容器中的 <code>3306</code> 端口；同一 Compose 网络中的其他容器则应通过 <code>database:3306</code> 连接数据库。</p>
<p>容器正常工作、应用开始在数据库中创建和存储数据后，还需要考虑数据持久化。如果数据只保存在容器的可写层中，那么容器被删除或重新创建时，这些数据就会丢失；单纯停止并重新启动同一个容器并不会删除数据。</p>
<p>为了避免数据随容器被删除，需要把数据存储在生命周期独立于容器的卷中。</p>
<p>卷就是为此而设计的。本例中的 <code>mysql-go:/var/lib/mysql</code> 表示把名为 <code>mysql-go</code> 的命名卷挂载到数据库容器内的 <code>/var/lib/mysql</code> 路径。</p>
<p>与前面在 <code>app</code> 容器中把网络设为 <code>go_book_api_net</code> 一样，这个数据库容器也要指定同一个网络。由于需要让容器相互通信，把它们放在同一个网络中是合理的。</p>
<h3 id="phpmyadmin"><code>phpMyAdmin</code> 容器</h3>
<p>本例还可以选择配置 <code>phpmyadmin</code> 服务。我发现使用数据库客户端会更加方便，因为它能让我轻松查看数据库的结构和内容。</p>
<pre><code class="language-yaml">phpmyadmin:
  image: phpmyadmin
  restart: always
  ports:
    - 9000:80
  environment:
    PMA_HOST: database
    PMA_ARBITRARY: 1
  depends_on:
    - database
  networks:
    - go_book_api_net
</code></pre>
<p>这个过程与配置前面几个容器时几乎相同。Docker 会拉取现成的官方 <code>phpmyadmin</code> 镜像，并使用它创建容器。</p>
<p>这里把 <code>restart</code> 设置为 <code>always</code>，表示容器退出后，Docker 通常会自动重启该容器；如果手动停止容器，则不会立即自动重启。</p>
<p>在主机，也就是本地环境上，可以通过 <code>9000</code> 端口访问这项服务。该端口会映射到容器内的 <code>80</code> 端口。</p>
<p>在 <code>environment</code> 配置中，<code>PMA_HOST</code> 告诉 phpMyAdmin 连接到名为 <code>database</code> 的主机，也就是数据库服务。之所以可以这样做，是因为两个服务位于同一个网络中，这一点可以从 <code>networks</code> 属性看出。<code>PMA_ARBITRARY: 1</code> 允许用户在 phpMyAdmin 登录页面手动填写数据库服务器的主机名。例如，如果以后配置了另一个数据库，但仍想通过 phpMyAdmin 连接它，就可以在登录页面中指定该主机。</p>
<p>数据库客户端依赖 <code>database</code> 服务，因此还需要在 <code>depends_on</code> 中指定这一点。</p>
<pre><code class="language-yaml">volumes:
  mysql-go:

networks:
  go_book_api_net:
    driver: bridge
</code></pre>
<p>Docker Compose 文件的最后一部分定义了前面配置服务时所用的命名卷和自定义网络。</p>
<p>在顶层 <code>volumes</code> 下定义名为 <code>mysql-go</code> 的命名卷，然后在需要使用该卷的服务中指定容器内的挂载路径。可以在数据库服务中看到它的用法：</p>
<pre><code class="language-yaml">volumes:
  - mysql-go:/var/lib/mysql
</code></pre>
<p>网络也采用相同的思路。这里定义了一个名为 <code>go_book_api_net</code> 的网络，加入该网络的容器可以相互通信。<code>driver</code> 选项用于指定网络驱动；<code>bridge</code> 会创建用户定义的桥接网络，为不同网络中的容器提供隔离，但默认仍允许容器访问外部网络。</p>
<h3 id="">一起运行所有服务</h3>
<p>在使用 Docker Compose 之前，你只有一个 Dockerfile，用来为 Go 应用构建镜像。使用 Docker Compose 后，你可以定义应用、数据库和 phpMyAdmin 三项服务，并由 Compose 创建相应的容器，让它们作为一个完整应用协同工作。</p>
<p>你可以把整套项目配置推送到 GitHub 等平台。其他人可以克隆、启动并运行该应用，而不必在本地电脑上安装 Go、MySQL 或 phpMyAdmin。不过，他们仍然需要安装 Docker。提交代码前，请确保 <code>.env</code> 等包含真实凭据的文件已经被排除。</p>
<p>要构建 Compose 项目中需要从本地源代码构建的服务镜像，可以使用 <code>docker compose build</code> 命令。本例只有 <code>app</code> 服务包含 <code>build:</code>，因此该命令只会构建应用镜像：</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/0040fbdc-c541-494f-af9b-664d6a00bc17.png" alt="集成开发环境的终端正在构建镜像" width="1871" height="959" loading="lazy"></p>
<p>再次查看 Docker Desktop 界面，可以看到已经构建了一个与 <code>app</code> 服务对应的新镜像。</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/736be9be-feb1-4888-8d15-c818e4683f4b.png" alt="Docker Desktop 中已经构建好的镜像" width="1913" height="363" loading="lazy"></p>
<p>要启动这些容器，可以使用 <code>docker compose up</code> 命令：</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/8ba14bb9-77d5-48a1-b574-54a848f54b1e.png" alt="集成开发环境的终端中正在运行的容器" width="1912" height="993" loading="lazy"></p>
<p>进入 Docker Desktop 的 <strong>Containers</strong> 选项卡，可以查看各个容器的状态：</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/82e3d54d-bfec-4cea-806a-c52846a3e077.png" alt="Docker Desktop 中正在运行的容器" width="1910" height="202" loading="lazy"></p>
<p><code>app</code> 服务的 <code>go_book_api</code> 容器并未保持运行，因为容器启动后，当前二进制文件只输出一行文字，执行完毕后几乎会立即退出。</p>
<p>修改 <code>main.go</code> 中的代码，创建一个监听 <code>8080</code> 端口的最小 HTTP 服务器：</p>
<pre><code class="language-go">// cmd/main.go
package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})

	log.Println("listening on :8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}
</code></pre>
<p>如果你刚开始学习 Go，也不必太担心上面的代码。它为 <code>/health</code> 端点注册了一个处理函数，并让 HTTP 服务器监听 <code>8080</code> 端口；访问该端点时，服务器会返回“ok”。</p>
<p>确认 <code>Dockerfile</code> 中包含以下指令，让容器启动时执行已编译的二进制文件：</p>
<pre><code class="language-dockerfile"># run the compiled binary when the container starts
CMD ["/docker-gs-ping"]
</code></pre>
<p>修改 <code>main.go</code> 后，需要重新构建并再次启动这些容器。现在可以看到，所有容器都正在运行：</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/3ddf3e15-87b8-4978-851f-d6179e323166.png" alt="Docker Desktop 中正在运行的所有容器" width="1555" height="202" loading="lazy"></p>
<p>单击 <code>go_book_api</code> 容器，可以看到服务器正按配置在 <code>8080</code> 端口上运行：</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/ddd07614-eb53-4bfc-b088-e824f651ef6c.png" alt="Docker Desktop 中正在运行的容器" width="1919" height="522" loading="lazy"></p>
<p>由于应用运行在 <code>8080</code> 端口，并且已经设置了 <code>/health</code> 端点，因此可以在浏览器中访问该端点并查看输出“ok”。</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/39a1ea3e-7cbf-4d46-9bbe-bf8053d48586.png" alt="浏览器中的健康检查端点显示 ok 响应" width="1086" height="163" loading="lazy"></p>
<p>此外，单击已映射的 <code>phpmyadmin</code> 端口，就可以通过本地的 <code>9000</code> 端口访问数据库客户端，并使用 <code>.env</code> 文件中设置的数据库凭据登录。</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/8d7de244-7268-4d17-a779-785feae389c4.png" alt="浏览器中的 phpMyAdmin 登录表单" width="1915" height="962" loading="lazy"></p>
<p>Docker Desktop 中另一个值得查看的功能是卷。你可以在 <strong>Volumes</strong> 选项卡中看到已经配置好的 <code>mysql-go</code> 卷。</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/66d1dde3-2fc1-48aa-b701-7504dba2007f.png" alt="Docker Desktop 中的 Volumes 选项卡" width="1910" height="254" loading="lazy"></p>
<p>你可以随时在 Docker 图形界面中打开这些卷或容器，查看其中的文件和日志，也可以尝试停止某个容器，观察其他容器会如何响应，等等。</p>
<p>完成这一整套设置后，你发现了什么？项目文件准备完成后，运行项目的人不需要在本地安装 Go、MySQL 或 phpMyAdmin，只需要安装 Docker，就能使用官方发布的镜像编排出一个完整的应用。这就是 Docker 的魅力。</p>
<h2 id="">总结</h2>
<p>Docker 一开始可能显得非常抽象，但只要理解它的根本用途，一切就会清晰得多。</p>
<p>本文介绍了 Docker 是什么、如何对一个基本的 Go 应用进行容器化，以及如何使用 Docker Compose 管理多个容器。</p>
<p>如果你很难理解 Dockerfile 为什么这样设置，或者为什么要按这样的顺序编写，不必一开始就执着于独自弄清每个细节。刚开始学习 Docker 时，我发现把它想象成编写一份食谱会更容易理解。如果构建镜像失败，就说明你可能遗漏了其中某个步骤。</p>
<p>如果你希望在本教程之外进一步了解 Docker，可以查看 <a href="https://docs.docker.com/">Docker 官方文档</a>。其中有许多非常出色的资源。我也建议你继续深入学习，因为本文只是初步介绍了容器化可以实现的众多功能。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python 高效数据处理：批处理与流式处理管道 ]]>
                </title>
                <description>
                    <![CDATA[ 每条数据管道在编写任何代码之前，都要做出一个根本选择：是按照计划分批处理数据，还是在数据到达时持续处理？ 这个选择——批处理还是流式处理——会决定下游所有环节的架构。你使用的工具、能对数据时效性作出的保证、错误处理的复杂程度，以及运行整套系统所需的基础设施，都直接取决于这个决定。 选错的代价很高。本来使用批处理就足够，却搭建了流式处理管道的团队，最终不得不为一个原本不需要复杂方案的问题维护复杂的基础设施。 而在实际用例需要实时处理时仍然搭建批处理管道的团队，往往会在最糟糕的时刻才发现两者之间的差距——例如利益相关者问起，为什么数据看板已经落后了六个小时。 本文将介绍批处理管道和流式处理管道究竟是什么、两者在架构和权衡方面有何不同，以及如何使用 Python 实现这两种模式。读完本文后，你将掌握一套清晰的判断框架，可以为遇到的任何数据工程问题选择合适的方案。 前置知识 为了顺利理解本文内容，请确保你具备以下基础：  * 有编写 Python 函数和使用模块的实践经验  * 熟悉 pandas DataFrame 和基本的数据操作  * 大致了解 ETL 管道的作用——抽取（e ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/efficient-data-processing-in-python-batch-vs-streaming-pipelines/</link>
                <guid isPermaLink="false">6aad341cd6857d046b7c69f5</guid>
                
                    <category>
                        <![CDATA[ 数据工程 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Tue, 15 Sep 2026 12:57:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2026/09/a8d3fc81-792f-4c6b-b93a-b7ccf0244eb7.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/efficient-data-processing-in-python-batch-vs-streaming-pipelines/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Efficient Data Processing in Python: Batch vs Streaming Pipelines Explained</a>
      </p><!--kg-card-begin: markdown--><p>每条数据管道在编写任何代码之前，都要做出一个根本选择：是按照计划分批处理数据，还是在数据到达时持续处理？</p>
<p>这个选择——批处理还是流式处理——会决定下游所有环节的架构。你使用的工具、能对数据时效性作出的保证、错误处理的复杂程度，以及运行整套系统所需的基础设施，都直接取决于这个决定。</p>
<p>选错的代价很高。本来使用批处理就足够，却搭建了流式处理管道的团队，最终不得不为一个原本不需要复杂方案的问题维护复杂的基础设施。</p>
<p>而在实际用例需要实时处理时仍然搭建批处理管道的团队，往往会在最糟糕的时刻才发现两者之间的差距——例如利益相关者问起，为什么数据看板已经落后了六个小时。</p>
<p>本文将介绍批处理管道和流式处理管道究竟是什么、两者在架构和权衡方面有何不同，以及如何使用 Python 实现这两种模式。读完本文后，你将掌握一套清晰的判断框架，可以为遇到的任何数据工程问题选择合适的方案。</p>
<h2 id="">前置知识</h2>
<p>为了顺利理解本文内容，请确保你具备以下基础：</p>
<ul>
<li>有编写 Python 函数和使用模块的实践经验</li>
<li>熟悉 pandas DataFrame 和基本的数据操作</li>
<li>大致了解 ETL 管道的作用——抽取（extract）、转换（transform）和加载（load）</li>
</ul>
<h2 id="">目录</h2>
<ul>
<li>前置知识</li>
<li>什么是批处理管道
<ul>
<li>使用 Python 实现批处理管道</li>
<li>批处理适合哪些场景</li>
</ul>
</li>
<li>什么是流式处理管道
<ul>
<li>使用 Python 实现流式处理管道</li>
<li>流式处理适合哪些场景</li>
</ul>
</li>
<li>关键区别一览</li>
<li>如何在批处理与流式处理之间做出选择</li>
<li>混合模式：Lambda 架构与 Kappa 架构</li>
</ul>
<h2 id="">什么是批处理管道</h2>
<p>批处理管道会将一组有明确边界且数量有限的记录作为一个整体进行处理，例如一个文件、一个数据库快照，或者一天内产生的交易记录。它会按照计划定期运行，例如每小时、每晚或每周一次，读取相应时间段内的全部数据，对数据进行转换，再将结果写入某个位置。之后，管道会停止运行，等待下一次启动。</p>
<p>这种模式的思路很简单：<strong>先收集，再处理</strong>。两次运行之间不会发生任何处理。</p>
<p>以零售行业的 ETL 为例，一条典型的批处理管道可能采用以下流程：</p>
<ol>
<li>在午夜从事务数据库中提取过去 24 小时内产生的所有订单</li>
<li>将订单数据与产品目录表和客户维度表连接</li>
<li>按地区和产品类别计算每日营收汇总数据</li>
<li>将结果加载到数据仓库中，供报表使用</li>
</ol>
<p>管道启动、完成处理，并生成一份完整、一致的昨日业务快照。分析师早晨开始工作时，数据仓库已经完成更新。</p>
<h3 id="python">使用 Python 实现批处理管道</h3>
<p>最简单的批处理管道可以是一个 Python 脚本，其中包含三个明确分离的阶段：抽取、转换和加载。</p>
<pre><code class="language-python">import pandas as pd
from datetime import datetime, timedelta

def extract(filepath: str) -&gt; pd.DataFrame:
    """从每日导出的文件中加载原始订单数据。"""
    df = pd.read_csv(filepath, parse_dates=["order_timestamp"])
    return df

def transform(df: pd.DataFrame) -&gt; pd.DataFrame:
    """清洗订单数据，并按地区汇总每日营收。"""
    # 只保留已完成的订单
    df = df[df["status"] == "completed"].copy()

    # 从时间戳中提取日期，以便进行分组
    df["order_date"] = df["order_timestamp"].dt.date

    # 聚合：计算每个地区每天的总营收、订单数量和平均订单金额
    summary = (
        df.groupby(["order_date", "region"])
        .agg(
            total_revenue=("order_value_gbp", "sum"),
            order_count=("order_id", "count"),
            avg_order_value=("order_value_gbp", "mean"),
        )
        .reset_index()
    )
    return summary

def load(df: pd.DataFrame, output_path: str) -&gt; None:
    """将汇总结果写入数据仓库（此处写入 CSV 文件）。"""
    df.to_csv(output_path, index=False)
    print(f"已将 {len(df)} 行数据写入 {output_path}")

# 运行管道
raw = extract("orders_2024_06_01.csv")
aggregated = transform(raw)
load(aggregated, "warehouse/daily_revenue_2024_06_01.csv")
</code></pre>
<p>下面逐步看看这段代码做了什么：</p>
<ul>
<li><code>extract</code> 读取一个表示每日订单导出数据的 CSV 文件。<code>parse_dates</code> 参数会让 pandas 将 <code>order_timestamp</code> 列解析为日期时间对象，而不是普通字符串——这一点对于转换阶段提取日期非常重要。</li>
<li><code>transform</code> 完成两项工作：首先过滤掉所有未完成的订单，例如退货和取消的订单；然后按照日期和地区对剩余订单进行分组，生成营收汇总数据。<code>.agg()</code> 调用会在一次处理中为每个分组计算三项指标。</li>
<li><code>load</code> 将结果写入目标位置。在生产环境中，这一步可能是向数据库插入数据或将数据上传到云存储，但无论具体目标是什么，所采用的模式都相同。</li>
</ul>
<p>这三个函数被有意分开。通过将抽取、转换和加载相互分离，每个阶段都可以独立测试、替换和调试。如果转换逻辑发生变化，你不需要修改抽取或加载代码。</p>
<h3 id="">批处理适合哪些场景</h3>
<p>在以下情况下，批处理管道通常是合适的选择：</p>
<ul>
<li><strong>数据时效性要求以小时而不是秒来衡量。</strong> 每日销售报告不需要每分钟更新一次，每周运行的营销归因模型更不需要。</li>
<li><strong>你需要处理大规模历史数据集。</strong> 将两年的交易历史数据回填到新的数据仓库，本质上就是一项批处理作业——数据已经存在、边界明确，而你希望在一次运行中尽可能高效地完成处理。</li>
<li><strong>一致性比低延迟更重要。</strong> 批处理管道会生成完整的时间点快照，输出中的每一行都基于相同的输入状态计算得出。这种一致性对于财务报告、法规合规，以及任何需要稳定、可复现数据集的下游流程都很有价值。</li>
</ul>
<h2 id="">什么是流式处理管道</h2>
<p>流式处理管道会在数据到达时持续处理数据，可以逐条处理记录，也可以采用较小的微批次。数据集不存在所谓的“终点”——管道会持续运行，不断从消息队列、Kafka 主题或 webhook 等来源消费事件，并在每个事件到达时立即进行处理。</p>
<p>这种模式的思路是：<strong>边收集，边处理</strong>。管道始终处于运行状态。</p>
<p>仍以零售行业的 ETL 为例，流式处理管道可以在订单创建时处理相应事件：</p>
<ol>
<li>用户在网站上下单后，系统将一个事件发布到消息队列</li>
<li>流式处理管道在几毫秒内消费该事件</li>
<li>管道对事件进行验证、扩充，并将其路由到下游系统</li>
<li>欺诈检测服务、库存系统和实时数据看板都会立即收到更新后的信息</li>
</ol>
<p>这与批处理存在根本区别：数据并不是静静地存放在文件中等待处理，而是在持续流动，管道必须跟上数据流动的速度。</p>
<h3 id="python">使用 Python 实现流式处理管道</h3>
<p>Python 的生成器函数天然适合用作流式处理管道的构建模块。生成器每次只产生一个值，并在两次 <code>yield</code> 之间暂停执行——这正好对应了数据到达时逐条处理记录、而不将全部数据加载到内存中的思路。</p>
<pre><code class="language-python">import json
import time
from typing import Generator, Dict

def event_source(filepath: str) -&gt; Generator[Dict, None, None]:
    """
    从文件中模拟订单事件流。
    在生产环境中，这里会从 Kafka 或消息队列消费事件。
    """
    with open(filepath, "r") as f:
        for line in f:
            event = json.loads(line.strip())
            yield event
            time.sleep(0.01)  # 模拟事件到达之间的延迟

def validate(event: Dict) -&gt; bool:
    """检查事件是否包含必需字段，以及字段值是否有效。"""
    required_fields = ["order_id", "customer_id", "order_value_gbp", "region"]
    if not all(field in event for field in required_fields):
        return False
    if event["order_value_gbp"] &lt;= 0:
        return False
    return True

def enrich(event: Dict) -&gt; Dict:
    """在将事件路由到下游之前，为其添加派生字段。"""
    event["processed_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
    event["value_tier"] = (
        "high"   if event["order_value_gbp"] &gt;= 500
        else "mid"    if event["order_value_gbp"] &gt;= 100
        else "low"
    )
    return event

def run_streaming_pipeline(source_file: str) -&gt; None:
    """在每个事件从数据源到达时对其进行处理。"""
    processed = 0
    skipped = 0

    for raw_event in event_source(source_file):
        if not validate(raw_event):
            skipped += 1
            continue

        enriched_event = enrich(raw_event)

        # 在生产环境中：发布到下游主题，或写入数据接收端
        print(f"[{enriched_event['processed_at']}] "
              f"订单 {enriched_event['order_id']} | "
              f"£{enriched_event['order_value_gbp']:.2f} | "
              f"等级={enriched_event['value_tier']}")
        processed += 1

    print(f"\n完成。已处理：{processed} | 已跳过：{skipped}")

run_streaming_pipeline("order_events.jsonl")
</code></pre>
<p>下面看看代码的具体工作方式：</p>
<ul>
<li><code>event_source</code> 是一个生成器函数——注意它使用的是 <code>yield</code> 关键字，而不是 <code>return</code>。每次执行到 <code>yield event</code> 时，函数都会暂停，并将一个事件交给调用方。管道处理完这个事件后，生成器才会继续运行并获取下一个事件。这意味着，无论数据流有多大，内存中一次都只会保留一个事件。<code>time.sleep(0.01)</code> 用于模拟现实中事件从消息队列陆续到达时的间隔。</li>
<li><code>validate</code> 会先检查每个事件是否包含必需字段，以及字段值是否有效，然后才进行其他操作。在流式处理场景中，异常事件非常常见——网络问题、上游系统缺陷和数据模式变更都可能产生格式错误的记录。尽早验证并跳过无效事件，要比让它们继续传播到下游系统安全得多。</li>
<li><code>enrich</code> 为事件添加派生字段。这里添加的是处理时间戳和订单金额等级。在生产环境中，这一步还可能与查询表连接、调用外部 API，或者应用模型预测结果。</li>
<li><code>run_streaming_pipeline</code> 将各个部分组合起来。针对 <code>event_source</code> 的 <code>for</code> 循环会逐个消费事件，让每个事件依次经过 <code>validate → enrich → route</code>（验证 → 扩充 → 路由）阶段，并持续记录已处理和已跳过的事件数量。</li>
</ul>
<h3 id="">流式处理适合哪些场景</h3>
<p>在以下情况下，流式处理管道通常是合适的选择：</p>
<ul>
<li><strong>数据时效性以秒或毫秒来衡量。</strong> 欺诈检测、实时库存更新、实时数据看板和告警系统都要求数据立即得到处理——如果批处理作业每小时才运行一次，这些系统就会失去作用。</li>
<li><strong>数据量大到不适合先积累再处理。</strong> 高频物联网传感器数据、点击流事件和金融行情数据每小时可能产生数百万条记录。先积累所有数据再处理通常并不现实——这需要巨大的存储空间，而且处理作业耗时过长，等结果出来时可能已经失去价值。</li>
<li><strong>你需要作出反应，而不只是生成报告。</strong> 流式处理管道可以针对单个事件触发下游操作，例如发送通知、阻止交易或更新推荐结果。批处理管道只能报告已经发生的事情。</li>
</ul>
<h2 id="">关键区别一览</h2>
<p>下面汇总了前文讨论的批处理与流式处理之间的区别：</p>
<table>
<thead>
<tr>
<th style="text-align:left"><strong>维度</strong></th>
<th style="text-align:left"><strong>批处理</strong></th>
<th style="text-align:left"><strong>流式处理</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left"><strong>数据模型</strong></td>
<td style="text-align:left">有边界、有限的数据集</td>
<td style="text-align:left">无边界、持续的数据流</td>
</tr>
<tr>
<td style="text-align:left"><strong>处理触发方式</strong></td>
<td style="text-align:left">计划任务（时间或事件）</td>
<td style="text-align:left">每条记录到达时触发</td>
</tr>
<tr>
<td style="text-align:left"><strong>延迟</strong></td>
<td style="text-align:left">几分钟到几小时</td>
<td style="text-align:left">几毫秒到几秒</td>
</tr>
<tr>
<td style="text-align:left"><strong>吞吐量</strong></td>
<td style="text-align:left">高（针对批量处理进行了优化）</td>
<td style="text-align:left">单条记录的开销较低</td>
</tr>
<tr>
<td style="text-align:left"><strong>复杂度</strong></td>
<td style="text-align:left">较低</td>
<td style="text-align:left">较高</td>
</tr>
<tr>
<td style="text-align:left"><strong>状态管理</strong></td>
<td style="text-align:left">每次运行通常无状态</td>
<td style="text-align:left">通常需要跨事件维护状态</td>
</tr>
<tr>
<td style="text-align:left"><strong>错误处理</strong></td>
<td style="text-align:left">重试整个作业</td>
<td style="text-align:left">针对单个事件使用死信队列</td>
</tr>
<tr>
<td style="text-align:left"><strong>一致性</strong></td>
<td style="text-align:left">强一致性（时间点快照）</td>
<td style="text-align:left">最终一致性</td>
</tr>
<tr>
<td style="text-align:left"><strong>最适合的场景</strong></td>
<td style="text-align:left">报表、机器学习训练、数据回填</td>
<td style="text-align:left">告警、实时功能、事件路由</td>
</tr>
</tbody>
</table>
<h2 id="">如何在批处理与流式处理之间做出选择</h2>
<p>这些信息固然很有用，但究竟应该如何在批处理和流式处理之间做出选择？这个决定主要取决于三个问题：</p>
<p><strong>数据需要保持多高的时效性？</strong> 如果利益相关者可以接受几小时前的结果，批处理更简单，也更具成本效益。如果他们需要在几秒内获得结果，那么流式处理就不可避免。</p>
<p><strong>处理逻辑有多复杂？</strong> 批处理作业可以连接大型数据集、执行计算成本较高的聚合，并应用复杂的业务逻辑，而不必担心延迟。流式处理管道必须快速处理每个事件，因此会限制单条记录能够承载的处理工作量。</p>
<p><strong>团队具备多强的运维能力？</strong> 流式处理基础设施——Kafka 集群、Flink 或 Spark Streaming 作业、死信队列，以及恰好一次（exactly-once）交付保证——其运维复杂度远高于按计划运行的 Python 脚本。如果团队规模较小，或者用例并不要求实时结果，那么这种复杂性只会增加成本，而不会带来实际收益。</p>
<p>从批处理开始。它更容易构建、测试、调试和维护。只有在某项具体、明确的需求——而不是对未来的假设——使批处理无法满足要求时，再转向流式处理。大多数数据问题都属于批处理问题，而真正需要流式处理的问题，通常会在你遇到时表现得非常明显。</p>
<p>正如你可能已经猜到的那样，有些数据处理系统需要将两种方式结合起来，这正是混合方案存在的原因。</p>
<h2 id="lambdakappa">混合模式：Lambda 架构与 Kappa 架构</h2>
<p>在实践中，许多生产环境中的数据系统会同时使用这两种模式。最常见的两种混合架构是 Lambda 架构和 Kappa 架构。</p>
<p><a href="https://www.databricks.com/glossary/lambda-architecture"><strong>Lambda 架构</strong></a>会并行运行一个批处理层和一个流式处理层。批处理层负责处理完整的历史数据，并在一定延迟后生成准确、一致的结果。流式处理层负责处理实时数据，并立即生成近似结果。下游消费者会合并两层的输出——利用流式处理结果保证时效性，利用批处理结果保证正确性。</p>
<p>这种架构的代价是运维复杂度：你需要维护两套独立的处理代码，并确保它们产生在语义上等价的结果。</p>
<p><a href="https://hazelcast.com/glossary/kappa-architecture/"><strong>Kappa 架构</strong></a>通过只使用流式处理层来简化系统，但当你需要进行类似批处理的重新处理时，它允许通过同一条管道重放历史数据。当 <a href="https://kafka.apache.org/documentation/">Apache Kafka</a> 和 <a href="https://flink.apache.org/">Apache Flink</a> 等流式处理框架支持日志保留和重放时，这种方案会很有效。你只需要维护一套代码和一套处理逻辑，并且可以在管道发生变化后重新处理历史数据。</p>
<p>这两种架构都不是在所有场景下更优。Lambda 架构更常见于那些先采用批处理、后来逐步加入流式处理的组织。Kappa 架构则更常见于从一开始就将流式处理作为主要模式来设计的系统。</p>
<h2 id="">总结</h2>
<p>批处理和流式处理是具有不同权衡的工具，各自适合不同类型的问题。批处理管道在一致性、简单性和批量吞吐方面表现出色；流式处理管道则擅长实现低延迟、事件响应和持续处理。</p>
<p>在选择 Apache Spark、Kafka 或 Flink 等具体框架之前，先从架构层面理解这两种模式，可以帮助你判断哪一种更适合当前问题，并清楚地解释这一选择。框架负责实现这些模式，而哪种模式适合你的问题，则需要由你先作出判断。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何从零开始实现一个 AI Agent CLI ]]>
                </title>
                <description>
                    <![CDATA[ 几个月前我决定从零开始造一个 AI Agent CLI——一个能像 Claude Code / Codex CLI / Gemini CLI 那样在终端里读代码、执行命令、修改文件的工具。 一个能演示核心循环如何运作的最小 demo，只有 50 行代码。它能在终端里跟用户对话、按需调用工具读文件。但 demo 也仅限于演示循环本身——要让 agent 跑在生产环境里，外面还要包一层工程框架：上下文压缩、权限模型、流式输出、错误恢复等等。这篇文章讲的就是如何把这一层外围工程拆开来看——一个 AI Agent CLI 从「能跑」到「能托付任务」之间，到底要补齐什么。 一、先看一个 50 行能跑的最小 agent 直接上代码。下面这段 TypeScript 用了 Vercel 的 AI SDK 和 DeepSeek 的模型，一共 50 行左右，已经是一个能在终端里和用户对话、能调用工具读文件的 agent： // hello-agent.ts import { deepseek } from '@ai-sdk/deepseek' ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-build-an-ai-agent-cli-from-scratch/</link>
                <guid isPermaLink="false">6a58cba9db3a41045b3a5390</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Mon, 01 Jun 2026 04:38:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2026/08/-----------AI-Agent-CLI.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>几个月前我决定从零开始造一个 AI Agent CLI——一个能像 Claude Code / Codex CLI / Gemini CLI 那样在终端里读代码、执行命令、修改文件的工具。</p><p><strong>一个能演示核心循环如何运作的最小 demo，只有 50 行代码</strong>。它能在终端里跟用户对话、按需调用工具读文件。但 demo 也仅限于演示循环本身——要让 agent 跑在生产环境里，外面还要包一层工程框架：上下文压缩、权限模型、流式输出、错误恢复等等。这篇文章讲的就是如何把这一层外围工程拆开来看——一个 AI Agent CLI 从「能跑」到「能托付任务」之间，到底要补齐什么。</p><h2 id="-50-agent">一、先看一个 50 行能跑的最小 agent</h2><p>直接上代码。下面这段 TypeScript 用了 Vercel 的 AI SDK 和 DeepSeek 的模型，一共 50 行左右，已经是一个能在终端里和用户对话、能调用工具读文件的 agent：</p><pre><code class="language-ts">// hello-agent.ts
import { deepseek } from '@ai-sdk/deepseek'
import { streamText, stepCountIs, tool } from 'ai'
import { z } from 'zod'
import fs from 'node:fs/promises'
import readline from 'node:readline'

const readFile = tool({
  description: '读取一个文本文件，返回完整内容',
  inputSchema: z.object({
    path: z.string().describe('要读取的文件路径'),
  }),
  execute: async ({ path }) =&gt; {
    return await fs.readFile(path, 'utf-8')
  },
})

async function main() {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
  const ask = (q: string) =&gt; new Promise&lt;string&gt;((r) =&gt; rl.question(q, r))
  const messages: Array&lt;{ role: 'user' | 'assistant'; content: any }&gt; = []

  for (;;) {
    const input = (await ask('\n你: ')).trim()
    if (!input || input === 'exit') break
    messages.push({ role: 'user', content: input })

    const result = streamText({
      model: deepseek('deepseek-chat'),
      messages,
      tools: { readFile },
      stopWhen: stepCountIs(10),
    })

    process.stdout.write('助手: ')
    for await (const chunk of result.fullStream) {
      if (chunk.type === 'text-delta') process.stdout.write(chunk.text)
      else if (chunk.type === 'tool-call') process.stdout.write(`\n  [调用 ${chunk.toolName}(${JSON.stringify(chunk.input)})]`)
      else if (chunk.type === 'tool-result') process.stdout.write(`\n  [返回 ${String(chunk.output).length} 字节]\n助手: `)
    }

    const { messages: newMessages } = await result.response
    messages.push(...(newMessages as any))
  }
  rl.close()
}

main().catch(console.error)
</code></pre><p>运行起来对话大致是这样的：</p><pre><code>你: 帮我看看 package.json 里有哪些依赖
助手:
  [调用 readFile({"path":"package.json"})]
  [返回 N 字节]
助手: 你的 package.json 里有以下依赖：ai、@ai-sdk/deepseek、zod ...
</code></pre><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/85de9400e6cd85cf9ded78bbe638c6c708d6b1d19f335f56ba1369396fb5f99f/68747470733a2f2f692d626c6f672e6373646e696d672e636e2f6469726563742f61393837626533353234303934663133613065633236356432366531316536662e676966" class="kg-image" alt="在这里插入图片描述" width="1327" height="628" loading="lazy"></figure><p>留意几件事。代码里没有任何地方写「如果用户问 package.json 就读它」、要不要调 <code>readFile</code>、调一次还是多次、什么时候停下来用自然语言回答，这些全部由模型自己决定。我们只是给它一个工具，然后开一个 while 循环。</p><p>Agent loop 的本质就是这样：<code>while 循环 + tool use 协议</code>。主流 AI CLI 的内核都是这个形态。区别在于：真正的产品在这 50 行代码外面，包了几千行甚至几万行的工程代码。</p><h2 id="-demo-">二、demo 与生产级产品之间，差的是什么</h2><p>demo 和生产级产品之间的差距非常大，它只能用来理解 agent loop 的概念，无法用于生产环境。我们可以挑 6 个典型场景来看看它们的差距：</p><p><strong>场景一：模型说「我帮你删一下这几个文件」，然后真的删了。</strong> demo 里的 <code>execute</code> 没有任何确认环节——模型一发起调用，工具就立刻执行。假如这条命令换成 <code>rm -rf node_modules</code>，文件就直接全删了。生产级产品需要权限系统，区分读和写，让用户在写操作之前看到将要执行的命令再决定要不要放行。</p><p><strong>场景二：用户按 Ctrl+C 想停下来，但是 shell 子进程没退出。</strong> demo 里的 <code>for await</code> 循环被强行中断后，API 请求停了，但工具发起的子进程（比如 <code>npm install</code>）还在后台运行。这是常见 bug——用户以为停了，但 CLI 还在占用 CPU、占用网络、写文件。要让 Ctrl+C 真正中断整条调用链，需要把 <code>AbortSignal</code> 从 Esc 按键一路下传到 <code>execa</code> 的 cancelSignal，再到 shell 子进程的 SIGKILL，中间任何一层断了都不行。</p><p><strong>场景三：多轮对话到了第 30 轮，请求就开始报错。</strong> messages 数组每一轮都追加新消息，每次请求都要把所有历史消息重发给模型。另外工具结果消耗的 token 也很多——<code>readFile</code>、<code>grep</code>、shell 命令的 stdout 几轮累积下来，导致 200K 上下文很快被填满。demo 既不压缩也不截断上下文，某一轮请求就会被供应商以 <code>context length exceeded</code> 拒绝，对话只能从头开始。</p><p><strong>场景四：模型陷入 doom-loop（反复重试同一个失败动作的死循环），导致 token 费用一直在涨。</strong> demo 里那行 <code>stopWhen: stepCountIs(10)</code> 是粗粒度兜底——agent loop 最多跑 10 步就会强制结束。但模型在 10 步内有可能反复尝试同一个工具：调 <code>readFile('a.txt')</code> 失败 → 调 <code>readFile('./a.txt')</code> 失败 → 调 <code>readFile('/a.txt')</code> 继续失败... 每一步都是一次 API 调用，每次都把越来越长的 messages 发回去。要让 loop guard（循环防护）真正阻断这种重复，需要在更细的粒度上工作——识别「同一个工具加同一组参数」的重复调用，到达阈值后合成一条引导消息让模型换思路。</p><p><strong>场景五：让 agent 在一个 8000 行的代码库里工作，它只能逐个 readFile 翻文件。</strong> demo 里只有 <code>readFile</code> 这一个工具。但实际开发中，agent 还需要更多工具：<code>grep</code> 搜索代码内容、<code>glob</code> 按文件名找文件、<code>writeFile</code> / <code>edit</code> 修改代码、<code>bash</code> 执行测试、<code>webFetch</code> 查文档。每加一个工具，都要单独打磨它的描述（description）措辞、参数 schema 和错误返回格式。description 写得不够精准，模型在功能相近的工具之间就会混淆——比如该调 <code>readFile</code> 的时候反而调了 <code>grep</code>。</p><p><strong>场景六：用户问「我们上次聊到哪里了」，agent 没有上次会话的任何记忆。</strong> demo 退出后， messages 数组只在内存里，没有持久化到磁盘，下次启动就是全新会话。要让对话能跨进程恢复，需要把对话历史持久化（JSONL 单文件 transcript 是常见做法），并提供 <code>/resume</code> 入口加载历史。再进一步还需要构建记忆系统——后台提取器从对话里自动提炼用户偏好和项目背景，整理成结构化条目，下次启动注入到 system prompt。</p><p>我们还可以继续列更多的场景：多模态附件怎么处理、子任务怎么委派给一个独立 context 的 sub-agent、Plan Mode 怎么做「只读探索 → 用户审批 → 写操作执行」的状态机、知识系统怎么从多个 <code>AGENTS.md</code> 文件分层合并...</p><p>但这些都不是 LLM 能力的问题，是工程问题。50 行 demo 缺的不是更聪明的模型，而是外面那一层能让模型在真实环境里稳定运行的工程能力。</p><h2 id="-ai-">三、一个新视角：AI 工程的三个层级</h2><p>模型只是个发动机。要让它在代码库里完成任务，外面要套一整套工程。这套工程分三层，跟最近几年话题热度的次序一致：2022-2023 提示词工程（Prompt Engineering）成为研究热点，2024 上下文工程（Context Engineering）独立成概念，2026 驾驭工程（Harness Engineering）正式独立成术语。按工程师介入位置由近到远来看看具体的区别：</p><!--kg-card-begin: html--><table role="table" style="box-sizing: border-box; border-spacing: 0px; border-collapse: collapse; margin-top: 0px; margin-bottom: 16px; font-variant: tabular-nums; width: max-content; max-width: 100%; display: block; overflow: auto;"><thead style="box-sizing: border-box;"><tr style="box-sizing: border-box; background-color: rgb(255, 255, 255); border-top: 1px solid rgba(209, 217, 224, 0.7);"><th style="box-sizing: border-box; padding: 6px 13px; font-weight: 600; border: 1px solid rgb(209, 217, 224);">层级</th><th style="box-sizing: border-box; padding: 6px 13px; font-weight: 600; border: 1px solid rgb(209, 217, 224);">工程师介入位置</th><th style="box-sizing: border-box; padding: 6px 13px; font-weight: 600; border: 1px solid rgb(209, 217, 224);">主要工作</th></tr></thead><tbody style="box-sizing: border-box;"><tr style="box-sizing: border-box; background-color: rgb(255, 255, 255); border-top: 1px solid rgba(209, 217, 224, 0.7);"><td style="box-sizing: border-box; padding: 6px 13px; border: 1px solid rgb(209, 217, 224);">提示词工程（Prompt Engineering）</td><td style="box-sizing: border-box; padding: 6px 13px; border: 1px solid rgb(209, 217, 224);">字符串</td><td style="box-sizing: border-box; padding: 6px 13px; border: 1px solid rgb(209, 217, 224);">写 system prompt、tool description、Chain-of-Thought 引导</td></tr><tr style="box-sizing: border-box; background-color: rgb(246, 248, 250); border-top: 1px solid rgba(209, 217, 224, 0.7);"><td style="box-sizing: border-box; padding: 6px 13px; border: 1px solid rgb(209, 217, 224);">上下文工程（Context Engineering）</td><td style="box-sizing: border-box; padding: 6px 13px; border: 1px solid rgb(209, 217, 224);">数据</td><td style="box-sizing: border-box; padding: 6px 13px; border: 1px solid rgb(209, 217, 224);">决定哪些内容进入 context window、怎么压缩 / 截断 / 选择</td></tr><tr style="box-sizing: border-box; background-color: rgb(255, 255, 255); border-top: 1px solid rgba(209, 217, 224, 0.7);"><td style="box-sizing: border-box; padding: 6px 13px; border: 1px solid rgb(209, 217, 224);">驾驭工程（Harness Engineering）</td><td style="box-sizing: border-box; padding: 6px 13px; border: 1px solid rgb(209, 217, 224);">代码</td><td style="box-sizing: border-box; padding: 6px 13px; border: 1px solid rgb(209, 217, 224);">搭建调用模型的整套系统——agent loop、工具、护栏、用户交互</td></tr></tbody></table><!--kg-card-end: html--><p>这三层不互斥，很多机制都跨两层甚至三层——比如 prompt caching 同时涉及上下文工程和驾驭工程，loop guard 同时涉及提示词工程和驾驭工程。判断一个机制「主要在哪一层」，对理解它的设计取舍有帮助。</p><h3 id="-2022-2023-">提示词工程：2022-2023 成为研究热点</h3><p>提示词工程关心的是「怎么说话」——用什么样的 prompt 让模型理解任务边界、按什么格式输出。2022 年 ChatGPT 上线后这一层迅速成为显学，学术界涌现了一批 prompting 技巧：</p><ul><li><strong>Chain-of-Thought（CoT）</strong>：让模型「先想再答」，在推理任务上准确率显著提升</li><li><strong>Self-Consistency</strong>：多次采样取多数，进一步压低误差</li><li><strong>ReAct</strong>：「思考 - 行动 - 观察」三步交替循环，给 tool use 类任务打下范式基础</li><li><strong>Tree of Thoughts</strong>：每步推理生成多个候选思维构成一棵树，用 BFS / DFS 搜出最优路径——适合 Game of 24 这类需要预判和回溯的题</li></ul><p>这股 prompting 研究热在 2023 年到顶。2024 年起调 prompt 的效果就明显减弱——主流模型给个粗略提示也能听懂意图，反复打磨 prompt 也不见多少提升。但提示词工程没消失，只是从「主战场」退居二线——一个工具的 description 写不到位，模型还是用不起来。当年那些技巧也没白学——都被新一代模型在训练时学会了，今天的模型不用再特意提示「先想再答」，这套推理早就内置了。</p><h3 id="-2024-">上下文工程：2024 才独立出来</h3><p>上下文工程关心的是「让它看到什么」——哪些内容进入 context window、按什么顺序、要不要先压缩。</p><p>这一层从提示词工程里独立出来比较晚。2025 年 6 月，Shopify CEO Tobi Lütke 先在 X 上发了一句「我更喜欢 context engineering 这个术语」（6 月 19 日），几天后 Karpathy 转推背书并补了一句「LLM 是 CPU，context window 是 RAM」（6 月 25 日），这两条推文把 context engineering 带进了更多开发者的视野。Anthropic 工程博客随后专门写了一篇《Effective context engineering for AI agents》。背景是 context window 从 4k 扩展到 200K 甚至 1M 之后，大家发现一个反直觉的事实：「放进去更多」并不等于「模型读到更多」。</p><p>2023 年的 <em>Lost in the Middle</em> 论文揭示了这件事——模型在长 context 上性能呈 U 型曲线，中段最弱。100K tokens 全放进 context 后，模型对开头和结尾的内容记得很清楚，但中间那一大段经常「忘记」。光放进去不够，得主动管理：</p><ul><li>哪些信息保留在 context 里，哪些过滤掉</li><li>长内容怎么截断，保留头部还是尾部</li><li>历史对话超长怎么压缩</li><li>怎么让 prompt cache 命中（前缀字节稳定是前提）</li></ul><p>再加一层背景：token 在多轮对话里有累计成本（API 计费按 token 算），每一轮对话都要把整段历史发出去，延迟和 cache 命中率也会跟着波动。一个真实运行的 agent，第 10 轮、第 30 轮、第 100 轮的 context 占用、cache 命中率、响应延迟变化轨迹完全不同。上下文工程要管的不是单轮的决策，而是要让 context 在长会话里不溢出、cache 一直命中、延迟保持稳定。</p><p>到 2024 年，这一层已经有了自己的工具链——截断策略、压缩流水线、cache 命中保护、知识注入顺序，每一项各有取舍。</p><h3 id="-2026-">驾驭工程：2026 才正式独立成话题</h3><p>这个词出现得最晚——2026 年 2 月由 HashiCorp 联合创始人 Mitchell Hashimoto 在博客里正式独立成术语（核心公式：Agent = Model + Harness）。Anthropic、OpenAI 等团队跟进得比较多。</p><p>「Harness」字面意思是「挽具」——给原本只会预测下一个 token 的模型套上一身能完成任务的骨架。agent loop、工具系统、护栏、错误恢复、用户交互、评测钩子——所有让 LLM 在生产环境里完成任务的外围工程，都归在这一层。</p><p>驾驭工程的边界其实比 agent loop 宽得多。前面第二节列的 6 个场景全都属于这一层，再加上流式输出怎么拆解成 UI 能消费的事件、网络抖动时怎么重试、prompt cache 怎么保持命中、跨平台终端兼容性等等更细的工程——每一条单独看都不起眼，但加起来就是「能跑的 demo」和「能用的工具」之间的差距。提示词工程做单点质量，上下文工程做信息密度，驾驭工程做整体可用性。</p><p>回头看那 50 行 hello-agent，它已经把提示词工程和上下文工程的最小形态实现了——工具有 description、messages 数组维护了上下文。它缺的几乎全是驾驭工程的东西：UI、权限、错误恢复、loop guard、压缩、子 agent、知识系统。</p><h2 id="-vs-">四、应用层 vs 模型层：价值在向外层转移</h2><p>过去三年，AI 领域大部分钱和注意力都在模型层——训练更大的模型、更强的 reasoning、更长的 context。这是合理的，发动机决定车的上限。但 2024 年起边际收益明显递减：GPT-4 到 GPT-5、Claude 3.5 到 Claude 4，每一代的提升仍然存在，但已经不是早期那种「换一代模型整个产品形态都要重做」的跨越式提升——基线能力对大多数应用已经够用。</p><p>反过来应用层的差距越来越明显。同样调 Claude API，一款专门做编程的 agent 跟自己写个聊天框直接调 Claude API，体验完全不同——区别不在模型，在外面那一层工程。模型公司自己也意识到了这一点，Anthropic 做 Claude Code，OpenAI 做 Codex CLI，Google 做 Gemini CLI，都是在模型之上补工程层。</p><p>对开发者来说，驾驭工程是当前最值得投入精力的方向。提示词工程已经有大量博客和课程在讨论，上下文工程也有相对成熟的方法论，但驾驭工程还没有「标准答案」——怎么设计 agent loop、怎么切工具粒度、怎么做 loop guard，每家产品的做法都不一样，这也是产品之间拉开差距的地方。</p><h2 id="-">五、驾驭工程具体在解什么问题</h2><p>下面这几节每个都是一个独立的工程领域，背后都有自己的设计取舍。</p><h3 id="agent-loop-">Agent Loop 引擎</h3><p>50 行 demo 里 <code>streamText()</code> 函数一行带过的事，在生产产品里会展开成一棵函数树。常见的做法是按"主循环 → 单轮处理 → 工具执行"三层组织：</p><pre><code>主循环
   │
   ├──── 检查上下文是否需要压缩（接近窗口上限就触发）
   │
   ├──► 单轮处理
   │      │
   │      ├── 调模型流式输出（streamText）
   │      │
   │      ├── 把流式 chunk 分发给 UI
   │      │     ├── text-delta   → 文本片段写入终端
   │      │     ├── tool-call    → 显示「调用 readFile(...)」
   │      │     └── tool-result  → 显示「返回 N 字节」
   │      │
   │      └── 把这一轮的 messages 累积进对话状态
   │
   ├──► 检查 finishReason
   │      ├── 'tool-calls'  → 执行工具 → 回填结果 → 继续下一轮
   │      ├── 'length'      → 注入 resume 提示 → 继续
   │      └── 'stop' / 错误 → 结束
   │
   └── 用户按 Esc → AbortSignal 一路下传 → 整条调用链中断
</code></pre><p>Agent Loop 引擎要解决的核心问题包括：把模型流式输出拆解成 UI 能消费的事件、把上一轮的 tool_result 配对返回、在出错时分类处理、在用户按 Esc 时把整条调用链彻底中断。</p><p>实际工程里错误有很多种，按处理策略大致可以归成四类。一类是可以静默重试的：429 限流、503 维护、网络抖动，等几秒重试即可。一类是重试也是同样结果，要立刻报给用户并提示下一步：401 鉴权失败提示更换 key、402 余额不足提示充值、422 内容审核提示改写内容。一类是不能当错误处理的，例如 context 超限要压缩上下文，不能直接中断会话。还有一类是 demo 没有覆盖的：上一轮历史里有孤立的 <code>tool_call</code> 缺少对应的 <code>tool_result</code>，下一次发请求供应商会直接拒绝，要在发送前主动修复工具调用的配对关系。</p><h3 id="--1">工具系统：模型的「手脚」</h3><p>模型不会自己读文件、执行命令、查询网络，凡是涉及磁盘、网络、shell 的操作，都需要工具系统去完成。模型在 messages 里发出 <code>调用 readFile('package.json')</code> 这样的指令，工具系统接收后把文件读取出来，再把内容塞回 messages 让模型读到。</p><p>执行本身没多少难度，真正费心思的是几处设计取舍：</p><p><strong>粒度划分。</strong> 给模型一个 <code>bash</code> 工具让它自己写命令是最灵活的方案，但模型可能写出意想不到的命令，安全性和可预测性都差。换成 <code>readFile</code> / <code>writeFile</code> / <code>grep</code> / <code>glob</code> 这种细粒度工具，行为可控，但又覆盖不到 bash 那样的灵活操作。Claude Code、Cursor 等主流产品基本都做混合：细粒度工具承担日常操作，bash 留给特殊场景。</p><p><strong>错误返回格式。</strong> 工具失败时，错误信息要写回 messages 让模型理解。这里要注意一下错误消息的尺度——如果消息内容过于简略，模型可能不知道该怎么修；如果消息内容过于详细，又会浪费 token。所以实际工程里通常 error code、一句解释、关键上下文这三样齐了就够用。</p><p><strong>结果截断。</strong> <code>grep</code> 一次返回 5000 行的情况并不少见，完整塞进 messages 不现实。可选策略包括按行截断保留头尾、按字节截断保留头部、或在中间插一条 <code>[...truncated, N lines omitted]</code> 让模型知道结果被处理过。具体怎么选取决于工具特性。</p><p><strong>Esc 信号的传递路径。</strong> 这一项最容易出问题。<code>AbortSignal</code> 要从用户按下 Esc 那一刻起，沿着「SDK 流式接收 → 工具执行 → 子进程封装 → 实际子进程」一路下传，每一层都要把信号正确转交给下一层：</p><ul><li>流式接收层：SDK 的 <code>streamText({ abortSignal })</code> 收到信号停止读取模型输出</li><li>工具执行层：拉起子进程的工具（比如 shell 工具）把信号转给下面的子进程封装</li><li>子进程封装层：通常用 <code>execa</code> 之类的库实现，调用 <code>spawn({ signal })</code> 把信号挂在 Node child_process 上</li><li>实际子进程：触发 SIGKILL，把整棵子进程树清理掉</li></ul><p>中间任意一层断开，子进程都会继续在后台运行——用户按下 Esc 之后 CLI 看似已经停止工作，实际上子进程还在写文件、查询网络、执行命令。</p><h3 id="-agent-">子 agent：把上下文当成稀缺资源</h3><p>子 agent 是一个相对新的设计点，跨在驾驭工程和上下文工程之间。</p><p>一个具体场景：用户问「这个项目里有哪些地方在用 <code>legacy_auth</code> 这个函数」。直观做法是让主 agent 调用 <code>grep</code> 工具在代码库里搜出所有包含 <code>legacy_auth</code> 的文件，然后对每个命中的文件调用 <code>readFile</code> 读出全文，再逐个判断该函数在那里的具体用法。问题是，这种探索性操作会把大量工具调用记录和文件内容留在父对话里，把后续做修改时需要的 context 提前占满。</p><p>子 agent 的思路是：把探索性子任务交给一个跑在独立 context 里的 sub-agent，子 agent 自己完成 <code>grep + 阅读 + 判断</code> 的整个流程，只把最终结论回传给父对话。从父对话视角看，相当于「问了一个问题，得到了一段总结」，中间几十次工具调用不进入主上下文。</p><p>这么做的好处首先是上下文隔离——探索过程中那些大量的工具调用和文件内容留在子 agent 内部，不挤占主对话的 context。其次是角色可以专精，探索型、规划型、审查型的 sub-agent 各自有自己的系统提示词和工具白名单。另外父子 agent 共享同一个 abort signal，用户 Esc 一次整棵任务树一起停。</p><p>设计子 agent 要解决的问题包括：父子之间用什么协议传递任务和结论、token 用量和权限信号怎么回流给父级、子 agent 在 Plan Mode 下要不要也强制只读、递归调用要不要禁止。多数实现选择禁止子 agent 再启子 agent，避免无限分裂。</p><p>从 token 成本看，子 agent 跑探索任务时，内部可能调几十次工具、产生数万 token 的工具结果，但回传给父对话的只有最终一段摘要——可能就几百 token。父对话里相当于「花了几百 token 换到一个结论」，子 agent 内部那几十次调用都封闭在自己的独立 context 里。在多轮长会话里这种「探索成本不污染主对话」的复利效果很明显，单纯的函数封装做不到。</p><h3 id="-ui-">终端 UI：最容易被低估的一层</h3><p>AI CLI 的终端 UI 需要同时承载若干交互形态：上方是滚动的对话历史，下方是固定的输入框，中间是流式打字效果。模型回复需要渲染 Markdown，权限敏感操作会触发确认对话框，CJK 字符、emoji、组合字符这些宽字符体系也都要支持。单独来看，每一项的实现难度都不大，但叠加起来就会发现有几处明显的难点：</p><p><strong>布局重排。</strong> 现代 TUI 框架里 Ink 直接复用了 React Native 的 Yoga 布局引擎，问题是 Yoga 对宽字符（CJK、emoji、组合字符）的列宽测算与终端实际渲染并不一致——一个汉字在终端里占 2 列，Yoga 默认按 1 列计入，整层 layout 计算都偏离实际列数。中文环境下流式输出每接收一个 chunk 都要重新跑一次布局，每次结果都带着累积偏差，具体表现为：上半屏文本的换行位置在每一帧之间跳动，下方固定输入框因为前面文本的行数被算错而上下漂移，光标也会在新旧两个候选位置之间闪烁。一段几百字的中文回复流式输出完，整个 UI 给用户的观感就是持续抖动。</p><p><strong>dynamic region 锚点冲突。</strong> 框架的局部重绘机制依赖终端的 cursor save 寄存器记录起始位置。一旦代码中有其他路径直接调用 <code>process.stdout.write</code>，该寄存器会被覆盖，重绘锚点随之错位。</p><p><strong>同步更新协议的兼容性。</strong> DEC private mode 2026 / BSU / ESU 这一组同步更新协议在新版 iTerm2、WezTerm、Windows Terminal 中已支持，但旧版 PowerShell、cmd 以及低版本 SSH 客户端尚未实现，需要在框架层提供 fallback。</p><p><strong>emoji 宽度对齐。</strong> 同一个 emoji 在不同终端中的列宽测算存在差异——部分按 2 列计算，部分按 1 列计算。这会导致版面对齐失效，需要维护一份 glyph 宽度表作为回退基准。</p><p>实际工程中，多数产品最终会绕开 Ink 等高层框架，直接操作 ANSI 转义序列，自行实现 cell-level diff 渲染。Claude Code、Codex CLI、Gemini CLI 的终端层都做了类似的工程取舍。</p><h3 id="prompt-caching-session-">Prompt caching：单 session 里最大的成本杠杆</h3><p>设想一个具体场景：你在用一个 AI 编程助手，它的 system prompt 有 8000 字，里面写着工作风格、可用工具、安全规则。每输入一句话，后端发给模型的请求都要带上这 8000 字 + 之前几轮对话 + 你这一句新输入。</p><p>问题是：这 8000 字 system prompt 从第一轮到第三十轮一字没变。模型每收到一次请求都要把它从头读一遍、算一遍，第二轮、第三轮做的工作跟第一轮完全重复，这部分重复计算最终会按 input token 全额结算到用户头上。</p><p>Prompt caching 就是针对这种浪费的服务端缓存方案。第一次收到这 8000 字 system prompt 时，供应商把模型处理它时算出的中间结果（具体是 Transformer 注意力层产生的 Key/Value 矩阵，行业里简称 K/V）存进缓存；下一次同样的前缀进来，跳过这段计算，只算新加的那句话。跳过的 token 按命中价结算，Anthropic / Google / Alibaba 显式缓存都在 input 价格的 <strong>10%</strong> 左右，DeepSeek 更激进只收 <strong>2%-3%</strong>，Moonshot 在 <strong>17%</strong> 上下——一段长 system prompt 在几十轮对话里反复复用，input 计费可以从 100% 压到 30% 甚至更低，这是单个 session 里最大的成本杠杆。</p><p>命中规则严格：<strong>前缀必须字节级一致</strong>。假如在 system prompt 里插入一段 <code>Current time: 2026-04-26 16:30:42</code> 这样的时间戳，会导致整段缓存作废，已积累的所有历史前缀也跟着按 100% 重新计费。</p><p>各家供应商的接入协议存在差异：</p><ul><li><strong>Anthropic</strong>：请求里显式设置 <code>cache_control</code> breakpoint，缓存命中范围覆盖 breakpoint 之前的完整前缀。</li><li><strong>OpenAI 兼容端点</strong>（DeepSeek / Moonshot / Alibaba 等）：服务端自动维护前缀缓存，识别到稳定前缀即自动命中（仍按命中费率结算，只是不需要在请求里额外声明）。</li><li><strong>Google</strong>：通过独立的 <code>cachedContent</code> API 接入，按存储时长和命中量分别计费。</li></ul><p>跨供应商运行的 agent 需要在请求发送层屏蔽这些差异，让上层 agent loop 不必感知当前对接的是哪一家。</p><p>另一处差异在最小缓存粒度：Anthropic 在 Claude 4.x 系列（Opus 4.5 / Haiku 4.5）上把起算线统一提到了 4,096 token，老版本 Claude 3.5 Sonnet 和 Opus 3 是 1,024，Claude 3.5 Haiku 是 2,048。前缀长度不达标则无法写入缓存。这一约束让 system prompt 的长度本身成为一个工程取舍点——太短触发不了缓存，过长则增加首次写入成本（Anthropic 默认 5 分钟缓存的写入费率比常规调用高约 25%，可选的 1 小时缓存高约 100%）。</p><h3 id="--2">知识与记忆：跨会话的上下文延续</h3><p>agent 默认是无状态的——每次启动都是一个空白会话，对项目背景和用户偏好一无所知。要让它在长期使用中持续累积项目认知，需要把这两类信息从单次 session 里抽出来做持久化存储。实际做法通常分两条线：一条是开发者主动维护的显式知识，另一条是 agent 在后台自动提炼的隐式记忆。</p><p><strong>显式知识：AGENTS.md 约定。</strong> 这是开源社区在 2025 年逐步形成的公开规范，目前由 Linux Foundation 下的 Agentic AI Foundation 托管，已被 OpenAI Codex、Cursor、Aider、Windsurf、Devin 等多家产品原生支持。开发者在项目根目录的 <code>AGENTS.md</code> 里写明代码风格、技术栈、模块划分、常用命令等项目级背景，agent 启动时把它注入到 system prompt 的固定位置，效果上等同于让 agent 在动手前先读一遍项目说明书。</p><p><strong>多层合并。</strong> 实际工程里这个注入要按层级组合：用户主目录下的全局偏好（个人风格，跨所有项目生效）→ 项目根目录的团队约定（共享给所有协作者）→ 子目录的局部覆盖（monorepo 里某个子包可以单独覆盖父目录的设置），子层级优先级高于父层级。再额外加一层 <code>.local</code> 文件（被 <code>.gitignore</code> 排除）承载个人在本仓库的私有偏好，不污染团队共享配置。同一份 agent 实现在不同项目、不同开发者手上能呈现出完全不同的工作方式，不需要每次启动时重新声明上下文。</p><p><strong>自动记忆：后台 extractor。</strong> 每轮对话结束后，后台跑一个轻量提取器，从对话内容里识别可复用的事实并按类别归档。常见的分类维度包括用户偏好（「注释写简略一点」）、项目背景（「这个仓库用 vitest 跑测试」）、协作习惯（「涉及数据库改动前先让我确认」）、外部引用（「监控面板在 grafana.internal/...」）等。提取出来的条目按一定的格式持久化到本地存储，下次启动时跟显式知识一起注入 system prompt。</p><p>两套机制配合使用之后，agent 在同一个项目上跑得越久，对项目结构和用户习惯的了解就越准确。那些让人感觉繁琐的事情——反复问已经说过的背景、再次建议已经被否决过的方案——会随着使用时间拉长逐渐消失。</p><h3 id="--3">生产化护栏</h3><p>最后一组是决定 agent 上生产环境后能不能稳定运行的护栏机制。下面三道分别对应三类典型故障——误删文件、token 成本失控、对话陷入死循环。</p><p><strong>权限模型。</strong> 核心思路是把读和写分开管控。一种常见的三档设计是：<code>default</code>（读操作直接放行、写操作弹确认对话框）、<code>plan</code>（强制只读，agent 只能探索代码、产出方案，所有写操作完全禁用）、<code>acceptEdits</code>（信任模式，所有写操作自动放行不再确认）。<code>plan</code> 这一档对应的工作流是「探索 → 出方案 → 用户审批 → 执行」，把模型的不确定性挡在写操作之前。</p><p><strong>上下文压缩。</strong> context 占用在多轮对话里会持续累积，溢出之前必须主动管理。一种常见的分级做法是：</p><ul><li>轻量压缩：O(n) 预清理，删除一些已经不再承载信息的中间消息（例如被截断后只剩外壳的工具结果）。</li><li>主动压缩：context 占用到 70-80% 时，把旧轮对话合并成一段摘要，腾出空间继续。</li><li>紧急压缩：context 即将溢出时强制截断旧消息。</li><li>手动 <code>/compact</code>：用户在合适的对话节点主动触发。</li></ul><p>每一级触发的阈值、保留范围、对 prompt cache 命中的影响都要精细设计——一次过早的主动压缩会让本可以命中的 cache 失效，反而加重 token 成本。这里还有一道隐性约束：被压缩进摘要的 <code>tool_call</code> / <code>tool_result</code> 必须配对清理干净，否则下一轮请求里出现一个孤立的 <code>tool_call</code> 引用，供应商会直接以 4xx 拒绝。</p><p><strong>Loop guard。</strong> 模型陷入死循环是真实发生的故障，不是假设场景。一个典型场景是 PowerShell 下的 shell 命令引号转义问题——模型试一遍失败，读了 stderr 之后重试同样的命令，又失败，再读 stderr 再重试 …… 每一轮都消耗 token、占用 context，永远拿不到正确答案。当前 LLM 在自回归生成 + 低温度采样下容易陷进这种循环：prompt 里反复出现「调 X 失败 → 我再调 X」会强化「调 X」这个 pattern，而模型本身没有元认知，并不知道自己上一轮做的是同一件事。</p><p>工程上的应对是给每次工具调用算一个稳定的指纹（典型做法是对工具名和参数做哈希），追加到一个固定的滑动窗口里。每次新调用进来，数一遍窗口里同指纹出现的次数，达到不同阈值触发不同动作。常见的双阈值设计是：软阈值（比如 3 次）注入一条合成 <code>tool-result</code>「这个调用已经失败过多次，换条思路」，让模型在 prompt 层面自己选择换条路径；硬阈值（比如 5 次）才弹对话框中断流程，由用户接管决策。双阈值给模型留一次自我纠正的机会，避免一次重复就硬切打断用户体验。</p><p>合成消息本身的措辞也是提示词工程——同样表达「换条思路」，措辞到不到位决定模型是继续硬试同样的参数，还是真正换一条路径。</p><p>这三道护栏分别对应一类典型故障：误删文件、token 成本失控、对话陷入死循环。生产级 agent 三项缺一不可。</p><h2 id="--4">六、整体工程量</h2><p>回头看整张清单：</p><pre><code>50 行 hello-agent
  → agent loop 引擎（错误分类 / 流式分发 / 配对修复）
  → 工具系统（10+ 工具 + 粒度设计 + 截断策略 + abort 传递）
  → 子 agent（独立 context + 工具白名单 + abort 级联）
  → 终端 UI（cell-level diff + 流式 + Markdown + 跨终端兼容）
  → 权限模型（三档 + plan mode 状态机）
  → 上下文压缩（三级 + cache 命中保护）
  → Loop guard（指纹检测 + 合成消息）
  → 知识与记忆（AGENTS.md 多层合并 + auto-memory）
  → 会话持久化（JSONL transcript + /resume）
  → 思考模式抽象 / 多模态附件 / Plan Mode 状态机 / ...
</code></pre><p>这就是从一个 50 行 demo 到一个能在真实工作流里跑起来的 AI CLI 之间需要补齐的工程缺口。核心代码量在两万行 TypeScript 量级。对比一个成熟 IDE 或一套云原生平台，这个体量并不大——一位有经验的工程师带 3-5 人的小团队，几个月可以做出第一版能用的产品。</p><p>两万行的工作量分布并不均匀。以我们做的这个开源实现为参考：Agent loop 本体加错误恢复在 1,000 行量级；工具系统和具体工具实现合起来 3,000-4,000 行；终端 UI（cell-level diff、Markdown 渲染、跨终端兼容、流式输出节流）单独占 2,000-3,000 行，是多数实现里被严重低估的一块。剩下的上下文压缩、权限模型、loop guard、知识系统、子 agent、会话持久化各自相对独立，但都要在主循环上挂载明确的对接点。</p><p>2025 年开始进入这条赛道的小团队明显增多——模型层的核心能力已经够了，剩下的瓶颈基本都在工程层。这一层的成熟度还在快速迭代，比如 AGENTS.md 这种跨产品的社区约定就是 2025 年才稳定下来的，再往前一年各家厂商还各自用 <code>CLAUDE.md</code> / <code>.cursorrules</code> / <code>.aider.conf.yml</code> 这些命名，互不兼容。</p><h2 id="--5">七、如果你想自己动手</h2><p>如果你也想自己造一个 agent CLI：</p><p><strong>先把 hello-agent 跑起来。</strong> demo 代码放在 <a href="https://github.com/woai3c/hello-agent">github.com/woai3c/hello-agent</a>。亲手跑一遍就能看清 agent 的核心结构——一个 while 循环、一组工具，模型自己决定调什么、调几次、什么时候停下来回答。本文讨论的所有工程问题，都是在这个最小循环外面一层一层加出来的。</p><p><strong>挑一个你最关心的问题深入。</strong> 不必从头读到尾，找一个最具体的问题先切进去。如果你最关心模型的边界在哪里，就从 agent loop 引擎和工具粒度入手；如果最担心 agent 失控（误删文件、token 账单爆炸），就从权限模型和 loop guard 入手。</p><p><strong>读真实的开源实现。</strong> Aider、OpenHands 都开源，每家在 UI、工具集、工作流上的取舍不一样，对照着读源码能看出不同设计思路。提醒一句：这些项目代码量在数万行级别，UI、工具、配置层互相牵扯，建议先选好一个具体问题（比如「Esc 是怎么把 shell 子进程一并杀掉的」），顺着这条主线从用户输入一路读到子进程退出，读完再换下一个问题。比从 <code>main</code> 函数一行行读下去高效得多。</p><p>想看每个机制在代码里具体怎么落地，可以看我写的一本小册：<a href="https://github.com/woai3c/x-code-cli"><strong>《从零打造一个 AI Agent CLI》</strong></a>。这本书以开源项目 <a href="https://github.com/woai3c/x-code-cli">X-Code CLI</a> 为主线，按本文「提示词工程 / 上下文工程 / 驾驭工程」的分法逐节展开。本文受篇幅所限只能讲到机制层，书里则把每个机制具体怎么在代码里落地展开来讲——agent loop 引擎、cell-level diff 渲染、子 agent 上下文隔离、loop guard 指纹算法、AGENTS.md 多层合并都各占一节。</p><h2 id="--6">参考资料</h2><ul><li>Vercel AI SDK 文档（<code>streamText</code> / <code>tool</code> / <code>stepCountIs</code>）：<a href="https://ai-sdk.dev/docs/reference/ai-sdk-core/step-count-is" rel="nofollow">ai-sdk.dev/docs/reference/ai-sdk-core/step-count-is</a></li><li>DeepSeek 开放平台：<a href="https://platform.deepseek.com/" rel="nofollow">platform.deepseek.com</a></li><li>Liu et al, <em>Lost in the Middle: How Language Models Use Long Contexts</em>（2023）：<a href="https://arxiv.org/abs/2307.03172" rel="nofollow">arxiv.org/abs/2307.03172</a></li><li>Anthropic 工程博客 · <em>Effective context engineering for AI agents</em>：<a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" rel="nofollow">anthropic.com/engineering/effective-context-engineering-for-ai-agents</a></li><li>Anthropic 工程博客 · <em>Building Effective Agents</em>：<a href="https://www.anthropic.com/engineering/building-effective-agents" rel="nofollow">anthropic.com/engineering/building-effective-agents</a></li><li>Tobi Lütke 关于 context engineering 的推文（2025-06-19）：<a href="https://x.com/tobi/status/1935533422589399127" rel="nofollow">x.com/tobi/status/1935533422589399127</a></li><li>Andrej Karpathy 跟进 context engineering 的推文（2025-06-25）：<a href="https://x.com/karpathy/status/1937902205765607626" rel="nofollow">x.com/karpathy/status/1937902205765607626</a></li><li>Mitchell Hashimoto · <em>My AI Adoption Journey</em>（2026-02-05，Harness Engineering 术语首次提出）：<a href="https://mitchellh.com/writing/my-ai-adoption-journey" rel="nofollow">mitchellh.com/writing/my-ai-adoption-journey</a></li><li>METR · <em>Measuring AI Ability to Complete Long Software Tasks</em>（2025）：<a href="https://arxiv.org/abs/2503.14499" rel="nofollow">arxiv.org/abs/2503.14499</a></li><li>Wei et al, <em>Chain-of-Thought Prompting Elicits Reasoning in Large Language Models</em>（2022）：<a href="https://arxiv.org/abs/2201.11903" rel="nofollow">arxiv.org/abs/2201.11903</a></li><li>Wang et al, <em>Self-Consistency Improves Chain of Thought Reasoning</em>（2022）：<a href="https://arxiv.org/abs/2203.11171" rel="nofollow">arxiv.org/abs/2203.11171</a></li><li>Yao et al, <em>ReAct: Synergizing Reasoning and Acting in Language Models</em>（2022）：<a href="https://arxiv.org/abs/2210.03629" rel="nofollow">arxiv.org/abs/2210.03629</a></li><li>Yao et al, <em>Tree of Thoughts: Deliberate Problem Solving with Large Language Models</em>（2023）：<a href="https://arxiv.org/abs/2305.10601" rel="nofollow">arxiv.org/abs/2305.10601</a></li><li>Christian Parpart · Terminal Synchronized Output（DEC private mode 2026）：<a href="https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036">gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036</a></li><li>AGENTS.md 社区规范：<a href="https://agents.md/" rel="nofollow">agents.md</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 使用 Python 和多进程构建简历筛选系统 ]]>
                </title>
                <description>
                    <![CDATA[ 招聘合适的人选，往往从一项耗时的工作开始：筛选简历。如果你发布过职位招聘，一定体会过收件箱里有数百份申请，而你得花数小时手动审阅每份简历的烦恼。 本文将带你用纯 Python 搭建一个简历筛选系统，重点讲解核心编程概念和多进程的运用。你会创建一个自定义系统，通过将非结构化的简历文档转化为排名榜单，实现评估流程的自动化。 阅读完本指南后，你将能够：  * 解析文档：使用 Python 从 PDF 和 DOCX 简历中提取文本  * 提取信息：从简历内容中识别技能与关键词  * 设计评分算法：用加权逻辑客观地为候选人排序  * 构建 Web 界面：使用 Streamlit 完成  * 部署应用：在 Streamlit Cloud 上部署应用，供公众访问 按照本教程操作，你将构建一个能在几秒内处理数百份简历的工具。 源代码见 GitHub 仓库 [https://github.com/abdultalha0862/Resume_Parser_Project] 目录  * 前置要求  * 项目概览  * 系统如何工作  * ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/python-resume-screening-system/</link>
                <guid isPermaLink="false">69904195be64e2045fddd24a</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Sat, 14 Feb 2026 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2026/02/----_20260214181319_13_235.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/python-resume-screening-system/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Build a Résumé Screening System Using Python and Multiprocessing</a>
      </p><!--kg-card-begin: markdown--><p>招聘合适的人选，往往从一项耗时的工作开始：筛选简历。如果你发布过职位招聘，一定体会过收件箱里有数百份申请，而你得花数小时手动审阅每份简历的烦恼。</p>
<p>本文将带你用纯 Python 搭建一个简历筛选系统，重点讲解核心编程概念和多进程的运用。你会创建一个自定义系统，通过将非结构化的简历文档转化为排名榜单，实现评估流程的自动化。</p>
<p>阅读完本指南后，你将能够：</p>
<ul>
<li>解析文档：使用 Python 从 PDF 和 DOCX 简历中提取文本</li>
<li>提取信息：从简历内容中识别技能与关键词</li>
<li>设计评分算法：用加权逻辑客观地为候选人排序</li>
<li>构建 Web 界面：使用 Streamlit 完成</li>
<li>部署应用：在 Streamlit Cloud 上部署应用，供公众访问</li>
</ul>
<p>按照本教程操作，你将构建一个能在几秒内处理数百份简历的工具。</p>
<p>源代码见 <a href="https://github.com/abdultalha0862/Resume_Parser_Project">GitHub 仓库</a></p>
<h2 id="">目录</h2>
<ul>
<li>前置要求</li>
<li>项目概览</li>
<li>系统如何工作</li>
<li>系统架构</li>
<li>项目结构</li>
<li>步骤 1：搭建项目</li>
<li>步骤 2：构建简历解析器</li>
<li>步骤 3：构建关键词提取器</li>
<li>步骤 4：实现评分引擎</li>
<li>步骤 5：构建 Web 界面</li>
<li>步骤 6：测试系统</li>
<li>步骤 7：部署应用</li>
<li>总结</li>
</ul>
<h2 id="">前置要求</h2>
<p>要学习本教程，你需要具备：</p>
<ul>
<li>Python 基础知识（函数、循环、字典）</li>
<li>已安装 Python 3.8 或更高版本</li>
<li>会用 <code>pip</code> 安装包</li>
<li>代码编辑器（如 VS Code、PyCharm 或任意你喜欢的编辑器）</li>
</ul>
<h2 id="">项目概览</h2>
<p>本指南将开发一个系统：输入一个简历文件夹和一份职位描述（JD），系统会处理每份简历、提取相关信息，并根据候选人与职位要求的匹配程度计算得分。</p>
<h2 id="">系统如何工作</h2>
<p>项目包含四个核心组件：</p>
<ul>
<li><strong>简历解析器</strong>：读取 PDF 和 DOCX 文件并提取文本</li>
<li><strong>职位描述解析器</strong>：分析职位描述以识别所需技能</li>
<li><strong>关键词提取器</strong>：将简历内容与技能分类体系进行匹配</li>
<li><strong>评分引擎</strong>：使用加权算法对候选人排序</li>
</ul>
<h3 id="">评分公式</h3>
<p>使用的评分公式如下：</p>
<pre><code>总分 =
（必备技能 × 50%）+
（优先技能 × 25%）+
（经验 × 15%）+
（关键词 × 10%）
</code></pre>
<p>这样设计可以确保核心技能比次要关键词权重更高。</p>
<h3 id="">这种方法如何有助于减少偏见</h3>
<p>本系统基于预定义标准评估简历，而不是主观判断。每份简历都根据同一套必备技能、优先技能、经验指标和关键词进行打分。</p>
<p>由于所有候选人都使用同一套加权公式评估，写作风格、排版或个人偏好等主观因素不会影响排名。评分逻辑只关注简历与职位要求的匹配程度。</p>
<p>通过将评估过程标准化，系统促进了更一致、更客观的筛选，有助于在简历初筛阶段减少偏见。</p>
<h2 id="">系统架构</h2>
<pre><code>输入                    处理                       输出
─────                   ──────────                  ─────

简历 ──► 简历解析器 ──► 关键词提取器 ──┐
(PDF/DOCX)                             │
                                        ├──► 评分引擎 ──► 排名结果
职位描述 ──► 职位描述解析器 ────────────┘
(TXT/PDF)
</code></pre>
<p>系统遵循简单的“输入—处理—输出”流程。</p>
<p>简历和职位描述作为输入。简历解析器从每份简历中提取文本，职位描述解析器从职位描述中识别必备技能和优先技能。</p>
<p>提取出的简历文本随后传给关键词提取器，该模块根据预定义的技能分类体系匹配技能和关键词。</p>
<p>最后，评分引擎应用加权公式为每位候选人计算得分，并输出一份按分数排序的简历列表。</p>
<h2 id="">项目结构</h2>
<pre><code>resume_screening_system/
├── app.py                    # Streamlit Web 界面
├── main.py                   # 命令行界面
├── parsers/
│   ├── resume_parser.py      # PDF/DOCX 文本提取
│   └── jd_parser.py          # 职位描述解析
├── extractors/
│   └── keyword_extractor.py  # 技能与经验提取
├── matcher/
│   └── scorer.py             # 评分算法
├── data/
│   ├── config.json           # 评分权重配置
│   └── skills_taxonomy.json  # 技能数据库
└── requirements.txt          # 依赖
</code></pre>
<p>项目按清晰的模块化目录组织。解析逻辑、关键词提取和评分分别放在各自文件夹中，配置文件与数据单独存放，便于浏览、维护和扩展。</p>
<h2 id="1">步骤 1：搭建项目</h2>
<p>创建目录结构并设置虚拟环境：</p>
<pre><code class="language-bash">mkdir resume_screening_system
cd resume_screening_system
mkdir parsers extractors matcher data input output
python -m venv venv
</code></pre>
<p>然后激活虚拟环境：</p>
<pre><code class="language-bash"># Windows
source venv/Scripts/activate

# macOS / Linux
source venv/bin/activate
</code></pre>
<p>安装所需依赖：</p>
<pre><code class="language-bash">pip install PyPDF2 python-docx streamlit pandas
</code></pre>
<h2 id="2">步骤 2：构建简历解析器</h2>
<p>简历解析器针对不同文件格式使用不同的提取方法。</p>
<p>对于 PDF，解析器逐页打开文档，用 PDF 阅读器从每页提取文本，再合并成单个字符串供后续处理。</p>
<p>对于 DOCX，解析器读取文档中的每个段落，将段落文本拼接成一块。这样无论简历格式如何，都能得到一致的文本输出。</p>
<p>将简历统一转为纯文本后，关键词提取和评分等组件就能高效工作。</p>
<p><strong>文件：</strong> <code>parsers/resume_parser.py</code></p>
<pre><code class="language-python">def _extract_pdf(self, file_path: Path) -&gt; str:
    text = ""
    with open(file_path, "rb") as file:
        pdf_reader = PyPDF2.PdfReader(file)
        for page in pdf_reader.pages:
            page_text = page.extract_text()
            if page_text:
                text += page_text + "\n"
    return text.strip()

def _extract_docx(self, file_path: Path) -&gt; str:
    from docx import Document
    doc = Document(file_path)
    return "\n".join(
        para.text for para in doc.paragraphs
    ).strip()
</code></pre>
<h2 id="3">步骤 3：构建关键词提取器</h2>
<p>本项目使用 <a href="https://www.kaggle.com/datasets/snehaanbhawal/resume-dataset">Kaggle</a> 上的简历数据集，确保逻辑在真实职业数据上可用。关键词提取器通过扫描简历文本来识别技能。</p>
<p>简历文本先转为小写，以便不区分大小写地匹配。预定义的技能分类体系存储每个技能及其可能变体，提取器用这些变体与简历文本比对。</p>
<p>匹配时使用词边界，避免部分匹配（例如在 “JavaScript” 里匹配到 “Java”）。匹配到的技能存入集合以避免重复。</p>
<p>这种方式能在所有简历上实现一致、可控的技能识别。</p>
<p><strong>文件：</strong> <code>extractors/keyword_extractor.py</code></p>
<pre><code class="language-python">def extract_skills(self, text: str) -&gt; Set[str]:
    text_lower = text.lower()
    found_skills = set()

    for category, skills_dict in self.skills_taxonomy.items():
        for skill_name, variations in skills_dict.items():
            for variation in variations:
                # 防止 "Java" 匹配到 "JavaScript"
                pattern = r"\b" + re.escape(variation) + r"\b"
                if re.search(pattern, text_lower):
                    found_skills.add(skill_name)
                    break

    return found_skills
</code></pre>
<h2 id="4">步骤 4：实现评分引擎</h2>
<p>为得到客观排名，系统使用加权评分公式。</p>
<table>
<thead>
<tr>
<th>组件</th>
<th>权重</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>必备技能</td>
<td>50%</td>
<td>核心技术需求</td>
</tr>
<tr>
<td>优先技能</td>
<td>25%</td>
<td>差异化竞争力</td>
</tr>
<tr>
<td>经验</td>
<td>15%</td>
<td>专业深度</td>
</tr>
<tr>
<td>关键词</td>
<td>10%</td>
<td>领域熟悉度</td>
</tr>
</tbody>
</table>
<pre><code>总分 =
(S_req × 0.50) +
(S_pref × 0.25) +
(E_exp × 0.15) +
(K_key × 0.10)
</code></pre>
<p>评分引擎用上述权重为每份简历计算最终得分。</p>
<p>它统计简历中出现的必备技能、优先技能、经验指标和关键词数量，每个数量乘以其对应权重（必备技能权重最高），加权值相加得到单一分数，再按该分数对简历排序，生成候选人排名列表。</p>
<h2 id="5web">步骤 5：构建 Web 界面</h2>
<p>Streamlit 为简历筛选系统提供了简单的 Web 界面。</p>
<p>文本区域用于输入职位描述，文件上传器用于上传多份简历。点击按钮后，Streamlit 触发后端逻辑解析简历、提取数据并计算得分，结果在浏览器中展示，用户无需使用命令行即可完成筛选。</p>
<p><strong>文件：</strong> <code>app.py</code></p>
<pre><code class="language-python">import streamlit as st

jd_text = st.text_area(
    "在此粘贴职位描述：",
    height=300
)

uploaded_files = st.file_uploader(
    "上传简历文件：",
    type=["pdf", "docx", "txt"],
    accept_multiple_files=True
)

if st.button("筛选简历", type="primary"):
    st.success("正在处理简历...")
</code></pre>
<p>运行应用：</p>
<pre><code class="language-bash">streamlit run app.py
</code></pre>
<p>应用将运行在 <a href="http://localhost:8500">http://localhost:8500</a>。</p>
<h2 id="6">步骤 6：测试系统</h2>
<h3 id="">示例职位描述输入</h3>
<p>下面是一份可用于测试的示例职位描述：</p>
<pre><code>我们正在寻找一名具有扎实后端开发经验的高级 Python 开发工程师。

必备技能：
- Python
- Django
- REST API
- SQL

优先技能：
- PostgreSQL
- Docker
- AWS

经验要求：
- 3 年以上 Python 专业开发经验
- 有构建 Web 应用的经验
</code></pre>
<p>该输入帮助系统识别必备技能、优先技能和经验关键词，供评分引擎对简历排序。</p>
<pre><code class="language-bash">python main.py
</code></pre>
<h3 id="">示例输出</h3>
<pre><code>============================================================
筛选结果
============================================================
第 1 名：Alice Johnson | 得分：85.42/100 | 匹配：python、django、postgresql
第 2 名：Carol Davis   | 得分：72.50/100 | 匹配：python、django
</code></pre>
<h2 id="7">步骤 7：部署应用</h2>
<p>若要让系统对外可访问：</p>
<ol>
<li>将代码推送到 GitHub</li>
<li>打开 share.streamlit.io</li>
<li>选择你的 <code>app.py</code> 文件</li>
<li>部署应用</li>
</ol>
<p>应用将发布在：</p>
<pre><code>https://your-app-name.streamlit.app
</code></pre>
<h2 id="">总结</h2>
<p>在本教程中，你使用 Python 从零搭建了一个完整的简历筛选系统。通过结合文本处理、结构化评分和自动化，该项目展示了如何将手动简历筛选转变为高效、客观的流程。</p>
<p>该系统有助于减少偏见、节省时间，并更一致地评估候选人。Happy coding!</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 代码整洁实践：敏捷软件开发中的代码优化指南 ]]>
                </title>
                <description>
                    <![CDATA[ 构建可扩展的软件应用程序需要编写干净的代码，这种代码简单到任何开发人员都能理解。 在这篇文章中，我将解释并演示什么是干净的代码。然后，我将分享我最喜欢的代码整洁模式，用于构建现代敏捷应用程序。 我不会使用复杂的术语。我会用简单明了的 JavaScript 示例来展示核心概念。开门见山，这就是我的风格。 让我们开始吧。 目录  1. 坏代码的成本            2. 清洁编码者 vs. 混乱编码者            3. 如果你的代码一团糟，AI 也救不了你 🗑️           ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-clean-code-handbook/</link>
                <guid isPermaLink="false">69630570d350d0045e97cbae</guid>
                
                    <category>
                        <![CDATA[ 代码质量 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tsukistar ]]>
                </dc:creator>
                <pubDate>Sun, 11 Jan 2026 02:21:24 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2026/01/----_20260111101926_191_113.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/the-clean-code-handbook/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">The Clean Code Handbook: How to Write Better Code for Agile Software Development</a>
      </p><!--kg-card-begin: markdown--><p>构建可扩展的软件应用程序需要编写干净的代码，这种代码简单到任何开发人员都能理解。</p>
<p>在这篇文章中，我将解释并演示什么是干净的代码。然后，我将分享我最喜欢的代码整洁模式，用于构建现代敏捷应用程序。</p>
<p>我不会使用复杂的术语。我会用简单明了的 JavaScript 示例来展示核心概念。开门见山，这就是我的风格。</p>
<p>让我们开始吧。</p>
<h2 id="">目录</h2>
<ol>
<li>
<p><a href="#heading-the-cost-of-bad-code">坏代码的成本</a></p>
</li>
<li>
<p><a href="#heading-clean-coder-vs-messy-coder">清洁编码者 vs. 混乱编码者</a></p>
</li>
<li>
<p><a href="#heading-ai-cant-save-you-if-your-code-is-a-mess">如果你的代码一团糟，AI 也救不了你 🗑️</a></p>
</li>
<li>
<p><a href="#heading-12-clean-code-design-patterns-for-building-agile-applications">用于构建敏捷应用的 12 个整洁代码设计模式 ⚖️</a></p>
<ul>
<li>
<p><a href="#heading-use-names-that-mean-something">🌿 使用有意义的名称</a></p>
</li>
<li>
<p><a href="#heading-keep-functions-laser-focused-srp">🔨 保持函数的专注性 (SRP)</a></p>
</li>
<li>
<p><a href="#heading-use-comments-thoughtfully">🚪 谨慎使用注释</a></p>
</li>
<li>
<p><a href="#heading-best-practices-for-writing-good-comments">⚡ 编写优秀注释的最佳实践</a></p>
</li>
<li>
<p><a href="#heading-make-your-code-readable">🧩 让你的代码可读</a></p>
</li>
<li>
<p><a href="#heading-test-everything-you-write">🏌️ 测试你写的所有内容</a></p>
</li>
<li>
<p><a href="#heading-use-dependency-injection">💉 使用依赖注入</a></p>
</li>
<li>
<p><a href="#heading-clean-project-structures">📂 清理项目结构</a></p>
</li>
<li>
<p><a href="#heading-be-consistent-with-formatting">🤹‍♂️ 格式保持一致</a></p>
</li>
<li>
<p><a href="#heading-stop-hardcoding-values">✋ 停止硬编码值</a></p>
</li>
<li>
<p><a href="#heading-keep-functions-short">🤏 保持函数简短</a></p>
</li>
<li>
<p><a href="#heading-follow-the-boy-scout-rule">⛺ 遵循童子军原则</a></p>
</li>
<li>
<p><a href="#heading-follow-the-openclosed-principle">🏟️ 遵循开放/封闭原则</a></p>
</li>
</ul>
</li>
<li>
<p><a href="#heading-modern-best-practices-to-help-you-write-clean-code-a-summary">帮助你编写整洁代码的现代最佳实践：总结 🥷</a></p>
</li>
<li>
<p><a href="#heading-automated-tools-for-maintaining-clean-code">保持代码整洁的自动化工具 ⚓</a></p>
<ul>
<li>
<p><a href="#heading-1-static-analysis">1️⃣ 静态分析</a></p>
</li>
<li>
<p><a href="#heading-2-automated-code-formatting">2️⃣ 自动代码格式化</a></p>
</li>
<li>
<p><a href="#heading-3-continuous-integration-ci-testing">3️⃣ 持续集成 (CI) 测试</a></p>
</li>
<li>
<p><a href="#heading-4-cicd-pipelines">4️⃣ CI/CD 流水线</a></p>
</li>
</ul>
</li>
<li>
<p><a href="#heading-the-role-of-documentation-in-agile-software-development">文档在敏捷软件开发中的角色 🚣</a></p>
</li>
<li>
<p><a href="#heading-conclusion">结论 🏁</a></p>
</li>
<li>
<p><a href="#heading-frequently-asked-questions-about-clean-code">关于代码整洁的常见问题 🧯</a></p>
</li>
</ol>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xh3j6ccn1hc3euc3lfyl.png" alt="敏捷软件开发梗图" width="3125" height="1250" loading="lazy"></p>
<p>在敏捷中，变化是唯一不变的，干净的代码是你的盔甲。它使你具有适应性、敏捷性，最重要的是，它使你能够掌控局面。</p>
<p>事实是：如果你想在软件开发行业中生存，编写整洁代码绝非可有可无，而是必须掌握的能力。幸运的是，我们人类通过努力和练习终能掌握整洁代码的精髓。</p>
<h2 id="heading-the-cost-of-bad-code">坏代码的成本</h2>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wdai6npb55j71sguj6kl.png" alt="混乱代码与整洁代码成本对比图表 by shahan" width="500" height="600" loading="lazy"></p>
<p>为了解释这个堆叠条形图，在初始开发阶段，坏代码比整洁代码的变更成本<strong>稍微</strong>高一些。</p>
<p>但是当我们进入维护和重构阶段时，这个差距显著扩大，坏代码的成本几乎是整洁代码的两倍。</p>
<p>到了代码变成遗留代码时，坏代码的变更成本达到100%——现在升级它非常昂贵，而整洁代码仍然更易于管理，仅为45%。</p>
<p>目前，美国关于软件低质量成本的最新分析仍是信息与软件质量联盟（Consortium for Information and Software Quality，CISQ，网站：cisq.org）发布的2022年报告。在这份报告中，经估计，2022年软件低质量给美国经济造成了至少2.41万亿美元的损失，其中技术债务（technical debt）约占1.52万亿美元。</p>
<p>你可以<a href="https://www.it-cisq.org/the-cost-of-poor-quality-software-in-the-us-a-2022-report/">在这里阅读更多内容</a>。</p>
<p>近期的讨论继续强调技术债务对软件质量和业务绩效的显著影响。</p>
<p>例如，<a href="https://vfunction.com/blog/how-to-manage-technical-debt">2024年的一项调查</a>指出，超过50%的公司认为技术债务占其IT总预算的四分之一以上。如果不加以解决，这确实会阻碍创新。</p>
<p>如你所见，毫无疑问，在软件开发中，坏代码是一个代价高昂的问题。</p>
<h2 id="heading-clean-coder-vs-messy-coder">清洁编码者 vs. 混乱编码者</h2>
<p>这是一个展示<strong>两种</strong>编码者历程的图表：</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c6ubf77uwipf4gtucw8q.png" alt="整洁代码 vs 坏代码图表" width="604" height="340" loading="lazy"></p>
<ul>
<li>
<p><strong>⚠️ 混乱编码者（红线）：</strong> 起步快但坠落得更惨。他们写的行数越多，问题也越多。</p>
</li>
<li>
<p><strong>⚡ 清洁编码者（蓝线）：</strong> 开始慢但保持一致。增长不会停止——它会加速。</p>
</li>
</ul>
<p>🫵 现在，你可以决定想走哪条线。</p>
<h2 id="heading-ai-cant-save-you-if-your-code-is-a-mess">如果你的代码一团糟，AI 也救不了你 🗑️</h2>
<p>当你陷入写代码的困境时，你可能会求助于 AI。但让我告诉你：如果你的代码一团糟，AI 也救不了你。</p>
<p>这就像是在沙子上建房子。没错，它会暂时站立，但一旦有强风或大浪，就会倒塌。</p>
<p>请记住：AI 只是一个工具。如果你不知道如何编写清晰、可扩展的应用程序，就是在为失败埋下伏笔。</p>
<p>我反复看到了这样的情况：那些熟悉五种编程语言的开发者，他们能构建应用程序、网站、软件，他们对算法和数据结构了如指掌。</p>
<p>但当面对大型项目或别人的混乱代码时，他们崩溃了。</p>
<p>他们就像一位能设计和建造自己飞机的航天工程师，但却不知道如何驾驶它们。他们在自己的代码中坠毁。</p>
<p>这曾经是我...... 从前的我。我会写上数千行代码，但却发现自己连上周写的东西都无法理解。对我来说，那是一片混乱。</p>
<p>然后我恍然大悟 —— 每个开发者都在为此苦苦挣扎。问题不在于我知道多少，而在于我如何组织和结构化我所知道的东西。换句话说，这取决于对编程艺术本身的理解。</p>
<p>我决定摆脱这个陷阱。经过五个月的密集工作——每天写作、设计和研究四到五个小时——我创造了当初学编程时梦寐以求的工具，一本完整的初学者指南：<strong>《整洁代码从零到一》</strong>。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737731329839/c4c862d9-7fdc-460a-ae2e-18b19468b6ec.png" alt="《整洁代码从零到一》封面图片：从混乱代码到杰作" width="1920" height="1080" loading="lazy"></p>
<p>如果你想了解更多关于这本书的信息，我会在本教程的结尾告诉你所有的细节。所以继续阅读以了解更多关于编写整洁代码的内容。</p>
<h2 id="heading-12-clean-code-design-patterns-for-building-agile-applications">构建敏捷应用程序的 12 种整洁代码设计模式 ⚖️</h2>
<p>如果你的代码不遵循这些现代整洁代码设计模式，你可能正在制造一个定时炸弹。这些模式是你的工具。掌握它们并享受项目成功的乐趣。让我一一展示给你看。</p>
<h3 id=""><strong>🌿 使用有意义的名称</strong></h3>
<p>将你的变量或函数命名为 b 或 x 并没有帮助。将它们称为何物以便于理解。以下是一个坏变量名与好变量名的例子：</p>
<pre><code>// 弱且模糊
let b = 5;

// 强且清晰
let numberOfUsers = 5;
</code></pre>
<p>那些写出不清晰名字的人不愿为自己的错误负责。不要成为那样的人。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736165724746/37b2edc3-3c68-47a8-ab6f-f131a2239a01.png" alt="Shahan创作的关于不良变量名和良好变量名的漫画" width="2000" height="800" loading="lazy"></p>
<h3 id="srp"><strong>🔨 保持函数的专注性 (SRP)</strong></h3>
<p>一个函数应该做到<strong>一件事</strong>——并做到完美。这就是单一职责原则（<strong>SRP</strong>）。</p>
<p>好的代码就像一把锤子。它只打一根钉子，不会打十根。例如，如果你招聘一个人来处理公司里的所有事情——财务、销售、市场营销、清洁工作等等——他们很可能会因为无法专注而惨败。同样的道理适用于你代码中的类。</p>
<p>🚧 当一个类或函数做超过一件事时，就会变成一团乱麻。调试它就像在倒着拼拼图。比如，如果你的类既要处理用户输入又要操作数据库，这不是多任务并行，而是逻辑混乱。将其拆分，遵循一个方法只做一件事的原则。</p>
<p><strong>🔥 我的法则：</strong> 你的代码为你工作。保持它精炼、专注且可控，否则它会控制你。以下是实现这一目标的方法：</p>
<pre><code>// 整洁代码：专注于单一任务
function calculateTotal(a, b) {
    return a + b;
}

function logTotal(user, total) {
    console.log(`User: ${user}, Total: ${total}`);
}

// 混乱代码：尝试做所有事情
function calculateAndLogTotal(a, b, user) {
    let total = a + b;
    console.log(`User: ${user}, Total: ${total}`);
}
</code></pre>
<p>🪧 任务混在一起，混乱也就随之而来。就这么简单。</p>
<h3 id=""><strong>🚪 谨慎使用注释</strong></h3>
<p>职业开发者中有一句名言：</p>
<blockquote>
<p>“代码自有其解释。”</p>
</blockquote>
<p>当有人走进房间时，你不会每次都解释门是做什么用的，对吧？你的代码也该如此。</p>
<p>加注释没错，但如果代码不加注释就看不懂，那代码本身可能就有毛病。</p>
<p>🪧 好的注释要讲“为什么”，而不是“怎么做”或“是什么”。如果开发者连代码是怎么工作的都看不懂，那他们更不可能理解背后的“为什么”。</p>
<p>以下是一些好的注释与坏的注释的简短例子。我还将向你展示一个编写清洁注释的实际项目。</p>
<p><strong>例子1：糟糕的注释 👎</strong></p>
<pre><code>// 将价格乘以数量以计算总数
const total = price * quantity;
</code></pre>
<p>这是一个<strong>糟糕的注释</strong>，因为它只是重复了代码已经说明的内容。代码 <code>price * quantity</code> 本身已经很清晰，因此这个注释没有增加任何有用的信息。</p>
<p><strong>好注释：👍</strong></p>
<p>如果代码清晰简明，<strong>你不需要注释。</strong></p>
<pre><code>const total = price * quantity;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736165891398/6a942ad7-5b09-4990-9c7f-95358dafcbf3.png" alt="Shahan创作的说明不必要注释与“无声注释”的图像" width="2000" height="800" loading="lazy"></p>
<p><strong>例子2：糟糕的注释 👎</strong></p>
<pre><code>// 检查用户是否登录
function isUserLoggedIn(session) {
    return !!session.user;
}
</code></pre>
<p>这个注释不好，因为它没有解释 <code>isUserLoggin()</code> 的存在原因。它只是解释了正在发生的事情。但我们已经知道这是一个身份验证函数。这个注释是浪费时间。</p>
<p><strong>好例子 👍</strong></p>
<pre><code>// 用户在访问受保护的资源之前已经通过认证
function isUserLoggedIn(session) {
    return !!session.user;
}
</code></pre>
<p>这是一个<strong>好的注释</strong>，因为它解释了代码存在的<strong>原因</strong>。它告诉我们函数在允许访问应用程序的敏感部分之前检查用户是否已认证。它关注的是更大的图景。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736166143011/b3ddae3d-41cf-4534-8f1a-af710579922c.png" alt="之前：“检查用户是否已登录”。之后：“用户在访问受保护的资源之前已经通过认证。”作者：Shahan。" width="2000" height="800" loading="lazy"></p>
<h3 id=""><strong>⚡ 编写优秀注释的最佳实践</strong></h3>
<ol>
<li>
<p><strong>解释“为什么”，而不是“什么”：</strong><br>
写注释是为了解释代码的目的或背景，而不是代码正在做什么。</p>
</li>
<li>
<p><strong>避免明显的注释：</strong><br>
不要为代码已经清晰的部分写注释。</p>
</li>
<li>
<p><strong>保持简短和精确：</strong><br>
写简洁的注释，方便阅读，并直接解释目的。</p>
</li>
<li>
<p><strong>定期更新注释：</strong><br>
过时的注释可能误导开发者，所以在代码更改时务必更新注释。</p>
</li>
</ol>
<p><strong>现实世界中的例子（使用好的注释） 🛒</strong></p>
<p>让我们把这些实践应用到一个真实项目中：一个大型电子商务应用程序。一个函数根据订单详情计算运输成本。下面是完整的代码，我将在下面解释每个注释：</p>
<pre><code>// 运输规则：
// - 订单超过 $100 免费运输
// - 低于 $100 的订单标准运输（$10）
// - 国际订单额外 $5

function calculateShipping(order) {
    let shippingCost = 0;

    // 检查订单是否符合免费运输条件
    if (order.total &gt;= 100) {
        shippingCost = 0; // 免费运输
    } else {
        shippingCost = 10; // 标准运输费用
    }

    // 为国际订单增加额外费用
    if (order.isInternational) {
        shippingCost += 5;
    }

    return shippingCost;
}

// 示例使用
const order1 = { total: 120, isInternational: false };
const order2 = { total: 80, isInternational: true };

console.log(calculateShipping(order1)); // 输出：0
console.log(calculateShipping(order2)); // 输出：15
</code></pre>
<p>在函数的开始，我们包含了一个注释来解释运输费用的规则。这使读者在不需要阅读完整代码的情况下获得逻辑概览。</p>
<pre><code>// 运输规则：
// - 订单超过 $100 免费运输
// - 低于 $100 的订单标准运输（$10）
// - 国际订单额外 $5
</code></pre>
<p>然后，第一个条件检查订单总额是否大于或等于 $100。这里的注释明确了为什么会应用免费运输。</p>
<pre><code>// 检查订单是否符合免费运输条件
if (order.total &gt;= 100) {
    shippingCost = 0; // 免费运输
}
</code></pre>
<p>第二个条件为国际运输应用附加收费。注释解释了为何增加额外费用。</p>
<pre><code>// 为国际订单增加额外费用
if (order.isInternational) {
    shippingCost += 5;
}
</code></pre>
<p><strong>为什么这些注释很好？</strong></p>
<p>想象一下你正身处一个 20 人的开发团队。六个月后，有人读到了 <code>calculateShipping</code> 这个函数。如果没有这些注释，他们可能得浪费大把时间去猜测为什么国际订单会有一笔额外费用。好的注释能阐明背后的原因，避免让人陷入数小时的烦躁和困惑中。</p>
<h3 id=""><strong>🧩 让你的代码可读</strong></h3>
<p>如果别人看你的代码感觉像是在“破译密码”，那你已经在给团队“埋坑”了。不信请看：</p>
<pre><code>// 干净：读起来像看故事一样
if (isLoggedIn) {
    console.log("Welcome!");
} else {
    console.log("Please log in.");
}

// 杂乱：感到困惑
if(isLoggedIn){console.log("Welcome!");}else{console.log("Please log in.");}
</code></pre>
<p>如果你的代码杂乱且难以阅读，它会让他人甚至是你自己感到困惑！想象六个月后回到自己写的代码却感觉像在读一种外语。可读性高的代码节省时间，减少错误，使每个人的生活更轻松。</p>
<p><strong>🍵 为什么可读性很重要？</strong></p>
<ol>
<li>
<p><strong>对于你自己：</strong> 当你在几周或几个月后重新访问代码时，整洁的代码能帮助你在不浪费时间弄清楚自己做了什么的情况下继续进行接下来的工作。</p>
</li>
<li>
<p><strong>对于你的团队：</strong> 如果其他人阅读你的代码，他们不应该像是在解谜题。整洁的代码让团队合作更顺畅，防止沟通不畅。</p>
</li>
<li>
<p><strong>更少的错误：</strong> 清晰的代码更易于调试，因为你能快速发现错误。</p>
</li>
</ol>
<p><strong>🧙‍♂️ 如何编写可读的代码</strong></p>
<p>让我们构建一个简单的程序来管理图书馆中的书籍。我们将使其整洁和可读，接下来我将分解这段代码：</p>
<pre><code>// 一个表示书籍的类
class Book {
    constructor(title, author, isAvailable) {
        this.title = title;
        this.author = author;
        this.isAvailable = isAvailable;
    }

    borrow() {
        if (this.isAvailable) {
            this.isAvailable = false;
            console.log(`You borrowed "${this.title}".`);
        } else {
            console.log(`Sorry, "${this.title}" is not available.`);
        }
    }

    returnBook() {
        this.isAvailable = true;
        console.log(`You returned "${this.title}".`);
    }
}

// 一个用于展示可用书籍的函数
function displayAvailableBooks(books) {
    console.log("Available books:");
    books.forEach((book) =&gt; {
        if (book.isAvailable) {
            console.log(`- ${book.title} by ${book.author}`);
        }
    });
}

// 示例使用
const book1 = new Book("The Clean Coder", "Robert Martin", true);
const book2 = new Book("You Don’t Know JS", "Kyle Simpson", false);
const book3 = new Book("Eloquent JavaScript", "Marijn Haverbeke", true);

const library = [book1, book2, book3];

displayAvailableBooks(library); // 显示可用的书籍
book1.borrow(); // 借阅一本书
displayAvailableBooks(library); // 再次显示可用的书籍
book1.returnBook(); // 归还这本书
displayAvailableBooks(library); // 最终列表
</code></pre>
<p>我们创建了一个 <code>Book</code> 类来表示每本书。它具有像 <code>title</code>、<code>author</code> 和 <code>isAvailable</code> 这样的属性来追踪其状态。</p>
<ul>
<li><code>borrow</code> 方法检查书籍是否可用。如果是，则将其标记为不可用并打印一条消息。</li>
<li><code>returnBook</code> 方法使书籍再次可用。</li>
<li><code>displayAvailableBooks</code> 函数循环遍历图书馆，只打印可用的书籍。</li>
<li>我们创建了三本书（<code>book1</code>、<code>book2</code>、<code>book3</code>）并将它们存储在一个 <code>library</code> 数组中。</li>
<li>我们借阅和归还书籍，展示了可用书籍列表如何变化。</li>
</ul>
<p>如你所见，可读代码不仅仅关乎风格。它能节省时间、防止错误，并确保你的代码在未来多年仍能发挥作用。</p>
<h3 id=""><strong>🏌️ 测试你写的所有内容</strong></h3>
<p>如果你不花时间编写测试，那么代码出故障时就别感到意外。如果你打算编写测试，请遵循这套单元测试策略，以便及早发现问题。</p>
<p><strong>什么是单元测试？</strong></p>
<p>具体来说，单元测试检查代码的各个部分（如函数或类）以确保其正常工作。就像在建造房子的墙之前检查每一块砖是否完好一样。</p>
<p>让我给你一个单元测试工作原理的例子：</p>
<pre><code class="language-javascript">class Calculator {
    add(a, b) { return a + b; }
    subtract(a, b) { return a - b; }
}

// 测试它（单元测试）
const calculator = new Calculator();
console.assert(calculator.add(2, 3) === 5, "加法失败");
console.assert(calculator.subtract(5, 3) === 2, "减法失败");
</code></pre>
<p>让我们来看看这段代码是怎么运行的：</p>
<p>首先，我们要先构建一个计算器类：</p>
<pre><code class="language-javascript">class Calculator {
    add(a, b) { return a + b; }
    subtract(a, b) { return a - b; }
}
</code></pre>
<p><code>Calculator</code> 类有两个方法：<code>add</code> 和 <code>subtract</code>。</p>
<ul>
<li><code>add(a, b)</code> 接受两个数字并返回它们的和。</li>
<li><code>subtract(a, b)</code> 接受两个数字并返回它们的差。</li>
</ul>
<p>接下来，我们设置测试：</p>
<pre><code class="language-javascript">const calculator = new Calculator();
</code></pre>
<p>在这里，我们创建一个 <code>Calculator</code> 类的实例以测试其方法。</p>
<p>然后我们编写测试用例：</p>
<pre><code class="language-javascript">console.assert(calculator.add(2, 3) === 5, "加法失败");
console.assert(calculator.subtract(5, 3) === 2, "减法失败");
</code></pre>
<p><code>console.assert(condition, message)</code> 检查条件是否为 <code>true</code>。如果是 <code>false</code>，则消息（“加法失败”或“减法失败”）将显示在控制台中。</p>
<ul>
<li>
<p><strong>第一次测试</strong>：<code>calculator.add(2, 3) === 5</code></p>
<ul>
<li>调用 <code>add</code> 方法，给出 <code>2</code> 和 <code>3</code>。</li>
<li>检查结果是否为 <code>5</code>。</li>
</ul>
</li>
<li>
<p><strong>第二次测试</strong>：<code>calculator.subtract(5, 3) === 2</code></p>
<ul>
<li>调用 <code>subtract</code> 方法，给出 <code>5</code> 和 <code>3</code>。</li>
<li>检查结果是否为 <code>2</code>。</li>
</ul>
</li>
</ul>
<p>那么如果出错了会发生什么呢？在这里解决出现的问题是相当简单的。在这种情况下，如果 <code>add</code> 或 <code>subtract</code> 方法不能正常工作，测试将失败。例如：</p>
<pre><code class="language-javascript">console.assert(calculator.add(2, 3) === 6, "加法失败");
</code></pre>
<ul>
<li>条件 <code>calculator.add(2, 3) === 6</code> 是 <code>false</code>。</li>
<li>控制台将显示：“加法失败”。</li>
</ul>
<p><strong>实际例子：测试一个登录系统 👥</strong></p>
<p>让我们测试一个简单的登录系统，看看单元测试在实际场景中如何工作。</p>
<pre><code class="language-javascript">class Auth {
    login(username, password) {
        return username === "admin" &amp;&amp; password === "1234";
    }
}

// 测试 Auth 类
const auth = new Auth();
console.assert(auth.login("admin", "et5t45#@") === true, "有效凭证登录失败");
console.assert(auth.login("user", "wrongpassword") === false, "无效凭证登录成功");
</code></pre>
<p>首先，创建 <code>Auth</code> 类：</p>
<pre><code class="language-javascript">class Auth {
    login(username, password) {
        return username === "admin" &amp;&amp; password === "1234";
    }
}
</code></pre>
<p><code>login</code> 方法检查用户名是否为 <code>"admin"</code> 且密码是否为 <code>"1234"</code>。如果两者都匹配，则返回 <code>true</code>，否则返回 <code>false</code>。</p>
<p>接下来，设置测试：</p>
<pre><code class="language-javascript">const auth = new Auth();
</code></pre>
<p>创建一个 <code>Auth</code> 类的实例。然后编写测试用例：</p>
<pre><code class="language-javascript">console.assert(auth.login("admin", "1234") === true, "有效凭证登录失败");
console.assert(auth.login("user", "wrongpassword") === false, "无效凭证登录成功");
</code></pre>
<ul>
<li><strong>第一次测试</strong>：检查有效凭证（<code>"admin"</code>，<code>"1234"</code>）是否成功。如果不成功，就会显示 “有效凭证登录失败”。</li>
<li><strong>第二次测试</strong>：检查无效凭证（<code>"user"</code>，<code>"wrongpassword"</code>）是否失败。如果没有失败，就会显示 “无效凭证登录成功”。</li>
</ul>
<p><strong>🌱 为什么测试会导致干净的代码：</strong></p>
<ol>
<li>
<p>为了提高代码的可测试性，你会自然而然地编写更短小、更专注的函数。</p>
</li>
<li>
<p>测试能验证你的代码在不同场景下的表现是否符合预期。</p>
</li>
<li>
<p>有了测试作为保障，你可以放心大胆地更新代码，因为你知道任何错误都会被测试及时发现。</p>
</li>
</ol>
<h3 id=""><strong>💉 使用依赖注入</strong></h3>
<p>将依赖硬编码就像在额头上纹上某人的名字——它是永久性的，可能是磨蚀性的，并且限制了你的选择。</p>
<p>那么，依赖注入做了什么？它通过将依赖关系作为参数传递来管理代码的关系。它是灵活、可适应且易于维护的。</p>
<p>为了演示其工作原理，我这里使用 Nodemailer 依赖项向用户发送电子邮件：</p>
<pre><code>// 依赖：使用 Nodemailer 发送电子邮件
const nodemailer = require('nodemailer');
function sendEmail(to, subject, message) {
    const transporter = nodemailer.createTransport({ /* 配置 */ });
    return transporter.sendMail({ from: "programmingwithshahan@gmail.com", to, subject, text: message });
}
</code></pre>
<p>⚠️ 为了避免风险，请确保避免将依赖关系<strong>硬编码</strong>。使用抽象或配置文件进行安全维护。</p>
<p>这只是一个例子。作为开发人员，你可能会使用数百个库或依赖项。</p>
<p>我并不是说你绝对不应该依赖依赖项/库，因为如今很难避免它们。但在将它们安装到你的编码项目中之前，你应该非常小心。</p>
<p>你应该检查组织的软件系统的安全性、性能、质量或功能。因为它们有时可能包含会毁掉你整个项目的风险。</p>
<p>🚧 永远要控制你的工具，不要让它们控制你。</p>
<h3 id=""><strong>📂 清理项目结构</strong></h3>
<p>一个组织良好的项目就像一座高端<strong>精品店</strong>与一堆<strong>垃圾堆</strong>的区别。</p>
<p>以下是每个文件夹的组织方式：</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9xwyg9iqqcybz21lsgxz.png" alt="Shahan 的整洁代码项目结构图" width="1563" height="1250" loading="lazy"></p>
<p>如果你的代码库像个杂物抽屉，那你已经给未来的自己制造了麻烦。</p>
<p>让我们浏览上面的干净项目结构，以更好地理解它：</p>
<p><strong>1.</strong> <code>myProjet/src</code></p>
<p>这是整个应用程序的主容器。应用所需的一切都存储在这个文件夹中。它有子文件夹，可以保持整洁并在一个地方管理。</p>
<p><strong>2.</strong> <code>components</code></p>
<p>这是存放应用所有可重用部分的地方。你可以在多个地方使用这些组件，而无需再次构建它们。</p>
<p><strong>3.</strong> <code>services</code></p>
<p>这是应用的“大脑”。它在前端和后端为幕后工作提供支持。<code>emailService.js</code>、<code>userService.js</code> 和 <code>productService.js</code> 是 <code>services</code> 文件夹的一些示例文件。</p>
<p><strong>4.</strong> <code>utils</code></p>
<p>这包含了所有运行应用程序所需的小工具，使你的生活更轻松。例如，<code>formatedate.js</code>、<code>validateEmail.js</code> 和 <code>generateId.js</code> 是一些常见的 utils 文件，用于为整个项目制作可重用的组件。</p>
<p><strong>5.</strong> <code>tests</code></p>
<p>根据惯例，测试文件通常位于项目根级别的 <code>src</code> 文件夹<strong>之外</strong>。这样可以将生产代码（<code>src</code>）与测试代码（<code>tests</code>）分隔开，使其更清晰易于管理。查看下面结构：</p>
<pre><code>myProject/
├── src/              # 生产代码
│   ├── components/
│   ├── services/
│   └── utils/
├── tests/            # 测试文件
│   ├── components/
│   ├── services/
│   └── utils/
├── package.json      # 项目配置
└── README.md         # 文档
</code></pre>
<p>一些开发人员可能更喜欢在 <code>test</code> 文件夹中创建一个测试文件来测试所有内容。遗憾的是，一开始它看起来很整洁，但随着项目的发展，你将不得不寻找和搜索特定代码块。这样显得杂乱无章，并可能产生意外的测试结果。因此，强烈建议在 <code>tests</code> 文件夹中将它们分解成多个测试文件。</p>
<p><strong>一个实际的例子 📧</strong></p>
<p>让我为你创建一个干净、耐用的项目结构，以便在任何将来可能从事的项目中应用。不用说，干净的项目结构是构建可维护项目的基础。</p>
<p>根据我们之前的发送电子邮件应用程序的示例，我们将为该应用程序编写一个干净的项目结构。我们希望构建一个向用户发送电子邮件的应用程序。此应用的干净项目结构应如下所示：</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6v6rlc5qiplgxz1h4dps.png" alt="Shahan 的邮箱应用整洁代码项目结构图" width="1563" height="1250" loading="lazy"></p>
<p>如你所见，我将每个子文件夹和文件都打包在应用的主容器 <code>src</code> 文件夹中。在 <code>src</code> 文件夹中，我们创建了 <code>components</code>、<code>services</code>、<code>utiles</code>。最后，我们在 <code>src</code> 文件夹外有一个可管理的 <code>test</code> 文件夹，以独立测试每个组件。这个测试文件夹与位于 <code>src</code> 文件夹中的生产代码没有任何关系。</p>
<h3 id=""><strong>🤹‍♂️ 保持格式一致性</strong></h3>
<p>别让你的代码看起来像是出自不同的十个人一样。请务必保持格式风格的一致性。</p>
<p>使用 <a href="https://prettier.io/">Prettier</a> 或 <a href="https://eslint.org/">ESLint</a> 等工具强制执行一致的风格。如果每个文件都看起来不同，你只是在制造没人愿意解决的混乱。</p>
<p>我认为格式一致性是编写整洁代码的最重要方面之一。</p>
<pre><code>// 始终使用2个空格进行缩进
function calculateArea(width, height) {
  if (width &lt;= 0 || height &lt;= 0) {
    throw new Error("维度必须是正数。");
  }
  return width * height;
}

// 添加有意义的空白以提高可读性
const rectangle = {
  width: 10,
  height: 20,
};

// 清晰的逻辑分离
try {
  const area = calculateArea(rectangle.width, rectangle.height);
  console.log(`面积: ${area}`);
} catch (error) {
  console.error(error.message);
}
</code></pre>
<p>让我们来分析一下，这段代码在哪些方面体现了“整洁之道”：</p>
<h4 id="1">1️⃣ 一致的缩进</h4>
<p>为什么用2个或4个空格？这很简洁、最小化，并且被许多JavaScript风格指南普遍接受。它不会让眼睛感到负担，并且代码结构清晰。如果你在这里用2个空格，那里用4个空格，就会让人感到困惑——而困惑时容易犯错。</p>
<h4 id="2">2️⃣ 有意义的空白：给代码留出呼吸空间</h4>
<p>在<code>rectangle</code>定义和<code>try</code>块之间的额外换行就像句子中的停顿——它让读者有时间去处理信息。</p>
<h4 id="3">3️⃣ 清晰的逻辑分离：模块化思维</h4>
<pre><code>try {
  const area = calculateArea(rectangle.width, rectangle.height);
  console.log(`Area: ${area}`);
} catch (error) {
  console.error(error.message);
}
</code></pre>
<p>看看代码逻辑是如何被分成清晰的部分：</p>
<ul>
<li>
<p>首先是计算（<code>calculateArea</code>函数）。</p>
</li>
<li>
<p>然后是输出（<code>console.log</code>）。</p>
</li>
<li>
<p>最后是错误处理（<code>catch</code>块）。</p>
</li>
</ul>
<p>每个任务都有其独立的空间和明确的职责。</p>
<h4 id="4">4️⃣ 可读的错误处理</h4>
<p>当你抛出错误或记录消息时，请整洁地格式化它们。不要使用模糊或隐晦的信息。看到这样的代码，开发者立刻就能知道出了什么问题。</p>
<pre><code>throw new Error("维度必须是正数。");
</code></pre>
<p><strong>🐦‍⬛ 常规的格式一致性建议：</strong></p>
<ul>
<li>
<p>在整个代码库中一致地使用2个或4个空格进行缩进。避免使用制表符，以维护在不同编辑器间的一致性。</p>
</li>
<li>
<p>将每行的字符数限制在最多100-120个以内，以防止水平滚动并提高可读性。</p>
</li>
<li>
<p>将相关逻辑集中在一起，用空行分隔代码块以突出强调其用途。</p>
</li>
<li>
<p>最后，避免过度对齐代码。相反，让缩进自然引导逻辑流程。</p>
</li>
</ul>
<h3 id=""><strong>✋ 停止硬编码值</strong></h3>
<p>硬编码值是一种懒惰的编码方式。以下是证明：</p>
<pre><code>// 糟糕：硬编码且僵化
function createUser() {
    const maxUsers = 100;
    if (currentUsers &gt;= maxUsers) throw "用户太多！";
}

// 简洁：动态且灵活
const MAX_USERS = 100;
function createUser() {
    if (currentUsers &gt;= MAX_USERS) throw "用户太多！";
}
</code></pre>
<p>你看，改变这个变量将来不会让你感到意外。你确切地知道在哪里可以找到它来改变不确定的值。</p>
<p>最好将固定值存储在全局配置（配置）文件中。</p>
<p>🪧 因此，无论如何避免硬编码。硬编码看似捷径，却可能让未来的自己（或他人）抓狂。</p>
<h3 id=""><strong>🤏 保持函数简洁</strong></h3>
<p>如果你的函数超过20行，可能它试图做得太多了。</p>
<p>短小精悍的函数总是能命中目标。</p>
<p>冗长的函数又乱又难读，而短小的函数则清晰且集中。以下是你应如何将大型函数拆解：</p>
<pre><code>function updateCart(cart, item) {
    addItemToCart(cart, item);
    let total = calculateTotal(cart);
    logTransaction(item, total);
    return total;
}

function addItemToCart(cart, item) {
    cart.items.push(item);
}
</code></pre>
<p>让我解释一下这段代码，以便你理解为何将大型函数拆分是一个明智的策略。</p>
<ol>
<li>
<p><strong>主函数：</strong> <code>updateCart()</code> 调用较小的辅助函数来处理特定的任务，如：</p>
<ul>
<li>
<p>将物品添加到购物车。</p>
</li>
<li>
<p>计算总价格。</p>
</li>
<li>
<p>记录交易细节。</p>
</li>
<li>
<p>最后，返回总价格。</p>
</li>
</ul>
</li>
</ol>
<p>这并不是一个试图做所有事情的长块代码，而是将任务委派给辅助函数。</p>
<ol start="2">
<li><strong>辅助函数：</strong> <code>addItemToCart()</code> 这个函数<strong>只</strong>负责将物品添加到购物车。如果你需要更改添加物品的方式（例如，检查重复项）。你只需编辑这个小函数，而不必在<code>updateCart</code>中一大块代码中寻找更改。这就是编写整洁代码函数的方式，令人愉悦和易于维护。</li>
</ol>
<p><strong>函数过长会怎样？ 💤</strong></p>
<p>假设你没有拆分<code>updateCart</code>函数。它可能看起来像这样：</p>
<pre><code>function updateCart(cart, item) {
    cart.items.push(item);
    let total = 0;
    for (let i = 0; i &lt; cart.items.length; i++) {
        total += cart.items[i].price;
    }
    console.log(`添加了${item.name}。总价现在是$${total}。`);
    return total;
}
</code></pre>
<p>这里的问题是什么？</p>
<ul>
<li>
<p>它试图做所有事情。</p>
</li>
<li>
<p>特别是当它变得更庞大时，难以阅读。</p>
</li>
<li>
<p>如果出了问题，你将浪费时间去找出问题的所在部分。</p>
</li>
</ul>
<p>现在，选择权在你手中：是继续坚持那种混乱的“全能一把抓”写法，还是开始实践“一个函数只做一件事”的整洁心态？</p>
<h3 id=""><strong>⛺ 遵循童子军规则</strong></h3>
<blockquote>
<p>离开营地时，要比你发现它时更整洁。</p>
</blockquote>
<p>让我来详细解释一下：你不能只顾着自己使用，却留下一个比之前更烂的摊子。那是极不负责任的行为。真正的专业人士总会想办法让事物变得比原来更好。</p>
<p>在编程中，这意味着 <strong>每当你触碰代码库，都要让它变得更好一点。</strong> 去清理冗余、重构混乱的部分、提升可读性。如果你不这么做，你只是在不断堆积垃圾，而这些“屎山”最终会崩塌并砸到你自己头上。</p>
<p>来看这个例子。在这里，我们没有选择优化，而是仅仅增加了一层又一层的复杂度：</p>
<pre><code>// 原始代码：难以阅读，变量命名不好
function calc(a, b) {
  let x = a + b;
  let y = x * 0.2;
  return y;
}

// 我们只是不断添加而未进行清理
function calcDiscount(a, b, discountRate) {
  let total = calc(a, b);
  let final = total - discountRate;
  return final;
}
</code></pre>
<p>之后：每次都有所改善。这是一个有纪律的程序员的工作方式——他们在改进中前行：</p>
<pre><code>// 改进后的代码：清晰的命名，重构以提高清晰度
function calculateSubtotal(price, quantity) {
  return price * quantity;
}

function calculateDiscountedTotal(price, quantity, discountRate) {
  const subtotal = calculateSubtotal(price, quantity);
  const discount = subtotal * discountRate;
  return subtotal - discount;
}
</code></pre>
<p>现在，任何人一目了然地看出发生了什么。因为我们将代码拆解为更小、更集中的函数。因此，增加新功能不会破坏现有功能。🏕️</p>
<h3 id=""><strong>🏟️ 遵循开放/封闭原则</strong></h3>
<p>这项设计原则建议：代码应当设计成允许扩展，而不必改变现有的基础。</p>
<p>你肯定希望是“增加功能”，而不是每升级一次就把代码“拆了重建”。为了适配新需求而去修改旧代码，简直就像每次买新家具都要把房子推倒重盖一样。这显然是不可持续的。</p>
<p>让我们来看看如何构建更聪明、更具扩展性的代码，让你在增加功能的同时，不会把其他部分搞坏。</p>
<h4 id="">重构前：违反原则的写法</h4>
<p>你写了一个处理支付的类——挺简单的，起初它只支持信用卡支付。</p>
<p>结果你老板突然冒出来说：“嘿，我们现在得接个 PayPal。”</p>
<p>因为你之前没心思学什么代码整洁之道，你的代码现在看起来就像一个从 1995 年过时企业级系统里爬出来的“意面怪物”。来，欣赏一下你亲手打造的这件“杰作”：</p>
<pre><code>class PaymentProcessor {
  processPayment(paymentType, amount) {
    if (paymentType === "creditCard") {
      console.log(`Processing credit card payment of $${amount}`);
    } else if (paymentType === "paypal") {
      console.log(`Processing PayPal payment of $${amount}`);
    } else {
      throw new Error("Unsupported payment type");
    }
  }
}

const paymentProcessor = new PaymentProcessor();
paymentProcessor.processPayment("creditCard", 100);
paymentProcessor.processPayment("paypal", 200);
</code></pre>
<p>唉！每新增一种支付方式（例如 Apple Pay、Google Pay 等），都需要修改 <code>processPayment</code> 方法。不用说，你在添加新功能时有可能破坏现有功能。如果你学过这个原则，你就不会陷入这个困境。</p>
<p>不要担心：我会帮你解决这个问题。首先，我们需要重构代码。我们将通过使用<a href="https://stackify.com/oop-concept-polymorphism/">多态</a>来扩展其功能，而不是修改现有的类：</p>
<pre><code>javascriptCopy code// 基础类
class PaymentProcessor {
  processPayment(amount) {
    throw new Error("processPayment() must be implemented");
  }
}

// 信用卡支付
class CreditCardPayment extends PaymentProcessor {
  processPayment(amount) {
    console.log(`Processing credit card payment of $${amount}`);
  }
}

// PayPal 支付
class PayPalPayment extends PaymentProcessor {
  processPayment(amount) {
    console.log(`Processing PayPal payment of $${amount}`);
  }
}

// 添加新的支付类型？只需扩展类！
class ApplePayPayment extends PaymentProcessor {
  processPayment(amount) {
    console.log(`Processing Apple Pay payment of $${amount}`);
  }
}

// 使用方法
const payments = [
  new CreditCardPayment(),
  new PayPalPayment(),
  new ApplePayPayment(),
];

payments.forEach((payment) =&gt; payment.processPayment(100));
</code></pre>
<p>现在，添加新的支付方式不再需要修改现有的 <code>PaymentProcessor</code> 类了，你只需要创建一个新的子类。这样一来，原有代码保持原封不动，意味着完全没有破坏现有功能的风险。</p>
<p>每种支付类型都有独立的类。比如添加 PayPal 支持，完全不会影响到老代码。现在你可以自信地回复老板：“没问题，我 5 分钟就能把这功能加上。” 升职加薪的机会正等着你呢！</p>
<p>我在我的书 <a href="https://codewithshahan.gumroad.com/l/cleancode-zero-to-one">从零到一的整洁代码</a> 中分享了更多的技巧。</p>
<h2 id="heading-modern-best-practices-to-help-you-write-clean-code-a-summary">帮助你编写整洁代码的现代最佳实践：总结 🥷</h2>
<p>现在，让我为你展示最佳实践，并总结这 12 条整洁代码设计原则，助你在敏捷应用开发中游刃有余。</p>
<h3 id="">🔎 常见代码异味及其修复方法</h3>
<ul>
<li>
<p>💊 重复代码： 如果你在复制粘贴代码，那你是在给自己挖坑。把代码抽离成函数，一次性把事做对。</p>
</li>
<li>
<p>🛤️ 过长方法： 如果一个方法长到需要滚动条，那它承载的逻辑就太多了。拆分它，让功能保持专注。</p>
</li>
<li>
<p>👑 万能对象： 没有任何一个类应该包揽所有活儿。简化职责，否则你的代码库迟早会变成一团乱麻。</p>
</li>
</ul>
<h3 id="">💬 高效注释实践</h3>
<ul>
<li>
<p>💭 何时注释： 只有在代码逻辑不够清晰时才写注释。如果代码已经很直观了，注释就是噪音。</p>
</li>
<li>
<p>🫗 清晰度： 注释应该解释 <strong>“为什么”</strong> 而不是“是什么”。如果你的代码非得靠注释才能读懂，那可能它写得太复杂了。</p>
</li>
<li>
<p>🌴 避免冗余： 别给显而易见的代码写注释。如果函数名是 <code>addNumbers</code>，就别再注释说它是用来“加数”的。</p>
</li>
</ul>
<h3 id="">🧼 整洁代码的重构技巧</h3>
<ul>
<li>
<p>🏭 提取方法：方法太臃肿？拆掉它。这不仅仅是为了整洁，更是为了掌控感。</p>
</li>
<li>
<p>🫕 重命名变量：如果变量名不能一眼看出用途，就改掉它。命名的精准度反映了思维的严谨度。</p>
</li>
<li>
<p>🍃 如果你的判断语句写得像代数题一样复杂，简化它。如果是 if (a == true)，直接写成 if (a)。</p>
</li>
</ul>
<h3 id="">🧪 测试与清洁代码</h3>
<ul>
<li>
<p>🧙 单元测试：像审讯嫌疑人一样测试每一行代码。不放过任何死角。</p>
</li>
<li>
<p>🏇 TDD（测试驱动开发）：先写测试。这不只是为了抓 Bug，更是为了在动笔写代码前，就搞清楚它到底该干什么。</p>
</li>
<li>
<p>🧽 清洁测试：测试代码也要写得和业务代码一样整洁。如果测试代码本身就乱七八糟，它就毫无参考价值。</p>
</li>
</ul>
<h3 id="">🐛 错误处理与整洁代码</h3>
<ul>
<li>
<p>⁉️ 异常处理：大胆使用异常。它们不仅能处理错误，还能让你的主逻辑免受错误处理代码的干扰。</p>
</li>
<li>
<p>🖍️ 快速失败：一旦发现异常，立即停止。别让错误雪球越滚越大，当场解决。</p>
</li>
<li>
<p>🚨 日志记录：记录日志要像记录犯罪现场一样。清晰、精准，且只记录必要的信息。</p>
</li>
</ul>
<h3 id="">🌱 代码审查及整洁代码</h3>
<ul>
<li>
<p>🚢 流程化：建立一套系统。拒绝“牛仔式”野路子开发。审查、评判、改进。</p>
</li>
<li>
<p>🔪 工具化：利用工具让审查变得轻松。工具不仅是为了纠错，更是为了培养规范性。</p>
</li>
<li>
<p>🧦 文化建设：营造一种“反馈是金”的文化。帮助团队学会如何给出和接受有建设性的批评。</p>
</li>
</ul>
<h2 id="heading-automated-tools-for-maintaining-clean-code">保持代码整洁的自动化工具 ⚓</h2>
<p>工具和自动化技术对于编写清洁代码非常有帮助。如果你没有使用合适的工具和自动化来节省时间，那就错过了机会。</p>
<p>你认为可以"凭眼力"判断代码质量？再想想吧。没有自动化，这会发生：</p>
<ol>
<li>
<p>👎 因为“太忙”而错过显而易见的错误。</p>
</li>
<li>
<p>🤕 你的代码在每个文件中看起来都不同，协作起来头疼。</p>
</li>
<li>
<p>🪦 部署失败，因为你跳过了关键测试。</p>
</li>
</ol>
<p>成功的开发者使用合适的工具来自动化代码和完成任务。以下是使用现代工具维护清洁代码的四种策略。</p>
<h3 id="1"><strong>1️⃣ 静态分析</strong></h3>
<p>静态分析实际上是一个代码检查器，它在早期找出潜在问题。最棒的是，它在<strong>运行前</strong>工作，捕获可能导致崩溃、停机或令人尴尬的错误。</p>
<h4 id=""><strong>它是如何工作的？</strong></h4>
<ol>
<li>
<p><strong>语法检查</strong>：检查代码中编写的语法是否正确。如果拼写错误或遗漏了闭合括号，它会立即指出。</p>
</li>
<li>
<p><strong>代码质量规则</strong>：像 ESLint 这样的工具强制执行规则，如一致的缩进、避免未使用的变量，以及遵循最佳实践。</p>
</li>
<li>
<p><strong>错误预防</strong>：识别逻辑错误，比如使用未定义的变量或进行无意义的比较。</p>
</li>
</ol>
<p>以下是静态分析的实际操作：</p>
<h4 id="">🚨 在静态分析之前：</h4>
<pre><code class="language-javascript">let sum = (a, b) =&gt; { return a + b; }
console.log(sume(2, 3)); // Typo, unnoticed until runtime
</code></pre>
<ul>
<li><strong>问题</strong>：<code>sume</code>中的拼写错误只有在运行代码时才会导致错误，这可能会导致令人沮丧的调试会话，或者更糟糕的是，在生产环境中中断应用程序。</li>
</ul>
<h4 id="eslint">🚑 使用 ESLint 之后的静态分析：</h4>
<pre><code>codeError: 'sume' is not defined.
</code></pre>
<ul>
<li><strong>解决方案</strong>：ESLint 立即标记拼写错误，你甚至在运行代码之前就发现了问题。早期捕获错误，节省了时间和麻烦。</li>
</ul>
<h3 id="2"><strong>2️⃣ 自动代码格式化</strong></h3>
<p>格式化前：</p>
<pre><code class="language-javascript">function calculate ( x , y ){ return x+ y;}
console.log( calculate (2,3 ) )
</code></pre>
<ul>
<li><strong>问题</strong>：不一致的空格和格式化使代码难以阅读。</li>
</ul>
<h4 id="prettier">使用 Prettier 之后：</h4>
<pre><code class="language-javascript">function calculate(x, y) {
  return x + y;
}
console.log(calculate(2, 3));
</code></pre>
<ul>
<li><strong>解决方案</strong>：自动应用干净、一致、专业的格式。不再挑剔空格或对齐问题。</li>
</ul>
<p>当然了，这些都是基本操作。我讲这些是以防万一，比如你得用记事本手撕代码，或者是在面试这种没有 IDE 助力的环境。</p>
<h3 id="3ci"><strong>3️⃣ 持续集成（CI）测试</strong></h3>
<p>CI 测试确保你对代码的每一次更改都得到自动验证。它就像一个安全网，可以捕捉开发过程中引入的错误。CI 工具在每次推送代码时运行测试，因此在部署后没有任何中断。</p>
<h4 id="ci"><strong>CI 测试如何工作？</strong></h4>
<ol>
<li>
<p><strong>变化触发</strong>：每次提交代码时，CI 工具（如 GitHub Actions、Jenkins）会运行自动测试。</p>
</li>
<li>
<p><strong>反馈</strong>：如果出现问题，它会立即提供反馈。</p>
</li>
<li>
<p><strong>防止代码破坏</strong>：只有干净且工作的代码会被合并到主分支。</p>
</li>
</ol>
<h3 id="4cicd">4️⃣ CI/CD 流水线</h3>
<p>我们也使用 CI/CD 流水线作为一个持续的过程，其中包括代码构建、测试和部署，而 CI 测试是这个过程中专注于自动化代码变更测试的部分。</p>
<p><strong>CI/CD 流水线与 CI 测试的区别：</strong></p>
<ul>
<li>
<p><strong>CI/CD 流水线：</strong> CI/CD 流水线将代码构建、测试和部署整合为一个单一过程。这个过程确保对主分支代码的所有更改都可以发布到生产环境。CI/CD 流水线可以减少部署时间、降低成本并改善团队协作。</p>
</li>
<li>
<p><strong>CI 测试：</strong> CI 测试是自动测试集成到中央代码库中的代码变更的过程。CI 测试专注于确保代码库的稳定性以及解决集成问题。CI 测试帮助开发者构建稳定的、无错误的软件并满足功能需求。</p>
</li>
</ul>
<p>🚧 这些就是 CI 测试和 CI/CD 流水线概念的真正含义。并不像想象中那么复杂。所以让我详细讲解一下使用 GitHub Actions 进行的 CI 测试，因为我们现在通常通过自动化工具运行测试。</p>
<h3 id="githubactionsci"><strong>⚡ 使用 GitHub Actions 进行持续集成（CI）测试</strong></h3>
<p>正如我之前所说，CI 工具会在每次您推送代码或打开拉取请求时运行自动化测试。这确保只有正常工作的、无错误的代码才会被合并到主分支。</p>
<h4 id="githubactionsci">如何使用 GitHub Actions 设置 CI 测试</h4>
<p><strong>步骤 1：创建您的代码库</strong></p>
<p>为您的项目设置一个 GitHub 代码库。然后，使用以下命令将代码推送到 GitHub：</p>
<pre><code>git init
git add .
git commit -m "Initial commit for CI Testing"
git branch -M main
git remote add origin https://github.com/codewithshahan/codewithshahan.git
git push -u origin main
</code></pre>
<p>或者您可以在不使用命令的情况下从 GitHub 账户创建一个新的代码库。只需登录到您的 GitHub 账户并访问仪表板。您会发现一个“New”按钮来创建一个全新的代码库：</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737618697327/dcef8be8-0d08-45d7-8000-34c4c65df425.png" alt="在 GitHub 上创建新代码库的图片由 Shahan 提供" width="1371" height="553" loading="lazy"></p>
<p><strong>步骤 2：添加一个 GitHub Actions 工作流</strong></p>
<p>导航到您代码库的 <strong>Actions</strong> 选项卡。要执行此操作，首先您必须访问 GitHub 上的代码库（创建代码库后您会找到链接）。在此示例中，我创建了一个名为“codewithshahan”的新代码库。在这里，看一下导航栏右侧的 <strong>Actions</strong> 选项卡。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737618879398/7c5aa37a-72be-4701-a8f8-9ea9e05c0d5d.png" alt="GitHub Actions 导航选项卡的图片由 Shahan 提供" width="1885" height="724" loading="lazy"></p>
<p>进入 Actions 选项卡后，向下滚动一点，您会找到 <strong>continuous integration</strong> 部分：</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737619002674/60003e57-f2b2-48f1-bef8-9bde39149faf.png" alt="GitHub Actions 页面上的 CI（持续集成）测试图片由 Shahan 提供" width="1496" height="508" loading="lazy"></p>
<p>选择一个适合您的工作流设置。我将在这个项目中使用 Node.js。</p>
<p>点击配置按钮后，会自动创建一个 <code>node.js.yml</code> 文件，您可以根据目标调整代码。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737619475568/74da6d46-c105-42c8-8662-fc72e9410bda.png" alt="GitHub 自动测试工作流代码段的图片由 Shahan 提供" width="1879" height="854" loading="lazy"></p>
<p>我不会详细说明如何修改 <code>.yml</code> 文件。这取决于您的项目目标和个人偏好。此外，这是一个完全不同的更广泛的话题，鉴于这篇文章已经相当长，我将在以后的文章中再做解释。目前，请坚持这些基础知识。</p>
<p>这个 CI 测试工作流非常适合现代应用程序开发。您的应用程序在包含关键功能（例如：暗模式）、在 GitHub 代码库中直接构建和部署应用程序时保持稳定。通过这种方式，您可以自信地推送代码，确保您的代码始终清晰并准备好投入生产。</p>
<h2 id="heading-the-role-of-documentation-in-agile-software-development">文档在敏捷软件开发中的角色 🚣</h2>
<p>如果您希望您的编码能力达到顶级水准，您需要了解如何编写好的文档。如果您认为文档只是匆忙写下代码如何工作，那么您错了。这是关于解释<strong>为什么</strong>它可以工作，而不仅仅是如何工作。这是大多数人忽视的一点。</p>
<h3 id="1">1. 🚡 创建<strong>有用的文档（解释为什么，而不仅仅是如何）</strong></h3>
<p>当您编写文档时，您不仅仅是写下一些关于如何使用代码的说明。您是在告诉下一个人（或者未来的自己）为什么这个代码段最初存在。这就是好文档和差文档的区别。</p>
<p>糟糕的文档会让人困惑。它们太模糊、太简单，并且没有回答重要的问题。如果您的文档不清晰，这可能意味着您的思考不清晰。您基本上是在说，“我不在乎你是否理解这个，它管用，直接用就行。”这没有帮助。</p>
<p>优秀的文档回答了棘手的问题：</p>
<ul>
<li>
<p>✅ 为什么您选择了这种方法而不是其他方法？</p>
</li>
<li>
<p>✅ 为什么这个函数存在？它解决了什么问题？</p>
</li>
<li>
<p>✅ 为什么您这样编写代码？</p>
</li>
</ul>
<h3 id="2">2. ⏳ <strong>保持文档更新（过时的文档比没有文档更糟）</strong></h3>
<p>过时的文档是最糟糕的。事实上，它可能比没有文档更糟。当你的文档与代码不同步时，你是在给未来的自己（或下一个必须处理它的人）带来巨大的麻烦。</p>
<p>每当代码发生变化，你的文档也必须随之更新。它必须能够反映代码的当前状态。千万不要留下那些过时的信息去误导未来的开发者（甚至是未来的你自己），那只会让他们感到困惑并浪费时间。如果某些内容不再相关，直接删掉它。过时的文档就像凌乱的大脑——它只会拖你的后腿。</p>
<p>要养成定期检查和更新文档的习惯。代码改动的那一刻，文档也必须同步更新。就这么简单。</p>
<h3 id="3">3. 🚆 <strong>整合注释（代码中的良好注释是文档的一部分）</strong></h3>
<p>听好了——代码里的注释应当与文档融为一体。优秀的注释不该是开发者的“拐杖”，仅仅因为他们在别处解释不清楚代码才拿来凑合。注释的作用是升华文档，而不是取而代之。</p>
<p>注释是文档的补充。你应该编写那种清晰易懂、几乎不需要解释的高质量代码；但当某些地方无法做到“一眼看透”时，再加入注释。记住注释的准则：解释 <strong>“为什么（Why）”</strong>，而不是 <strong>“怎么做（How）”</strong>。文档也是同理。别说废话，让代码自己“说话”。注释应当服务于文档的宏观大局，而不是给烂代码打补丁。</p>
<p>🪧 优秀的代码应该是自解释的。先去优化代码逻辑，如果仍有必要，再添加注释进行澄清。保持注释整洁、简短、直击要害。</p>
<p>如果你想写出整洁、高效且易维护的代码，文档就是关键。别再把文档当成事后才补的作业，或者填补空间的废话。它是代码的延伸——是你进行清晰高效沟通的方式。它是留给后人的路线图，更是你思维过程的体现。</p>
<h2 id="heading-conclusion">结论 🏁</h2>
<p>整洁代码并非可有可无的“加分项”，而是那些志在成为领导者的人的“必选项”。它关乎掌控力、效率，以及长期的持续改进。归根结底，它将助你在敏捷软件开发的博弈中脱颖而出。</p>
<p>🪧 如果你想真正精进自己的技艺，请编写整洁的代码，让效率自己证明一切。</p>
<h2 id="heading-frequently-asked-questions-about-clean-code">关于整洁代码的常见问题 🧯</h2>
<ol>
<li>
<p><strong>什么是干净的代码？</strong> 是指不会让你想把电脑扔出窗外的代码。</p>
</li>
<li>
<p><strong>为什么干净的代码在敏捷中很重要？</strong> 因为敏捷注重速度和变化，凌乱的环境中你不可能快速行动。</p>
</li>
<li>
<p><strong>什么是代码异味？</strong> 是你即将失去对代码库控制的迹象。</p>
</li>
<li>
<p><strong>我该如何改善注释？</strong> 只对必要的部分进行注释，并确保每个注释都增加价值而不是噪音。</p>
</li>
</ol>
<p>感谢您的陪伴。您可以访问我的 <a href="https://x.com/shahancd">Twitter 账号</a> 或 <a href="https://www.codewithshahan.com">我的网站</a> 阅读更多关于整洁代码和敏捷应用程序开发的文章。下次再见……继续改进你的代码库。</p>
<p>如果你认真想掌握整洁代码并提升你的编程职业生涯，我的书就是为你而写的：<a href="https://codewithshahan.gumroad.com/l/cleancode-zero-to-one"><strong>从零到一的整洁代码</strong></a>。这本书是你从零到一掌握整洁代码的完整指南，从杂乱到杰作。我正在提供50%的折扣，使用代码“earlybird”——仅限前50本。此外，还有30天的退款保证——无风险，纯奖励。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何成为 AI 辅助编程专家 - 开发者手册 ]]>
                </title>
                <description>
                    <![CDATA[ 在过去的七年里，我一直负责 freeCodeCamp 的基础设施。现在我确信，经验丰富的开发者可以在保持代码质量的同时提升 3-4 倍的编码速度。这就是 AI 辅助开发能够提供的优势。简单来说，使用像 GitHub Copilot 这样的 AI 工具作为你的编码伙伴可以提高效率。它们可以建议代码、帮助你调试以及加快处理重复性任务。 为什么这很重要 传统编码时，你需要自己编写每一行代码、查找文档、弄清语法。有了 AI，你可以：  * 专注于解决问题，而不是记住语法          * 通过实时看到优秀的代码示例更快地学习          * 快速构建项目而不牺牲质量         对于有经验的开发者来说，在 AI ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-become-an-expert-in-ai-assisted-coding-a-handbook-for-developers/</link>
                <guid isPermaLink="false">6953dfe138384c0457be01cb</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Fri, 09 Jan 2026 11:16:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2026/01/----_20260110202949_190_113.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-become-an-expert-in-ai-assisted-coding-a-handbook-for-developers/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Become an Expert in AI-Assisted Coding – A Handbook for Developers</a>
      </p><!--kg-card-begin: markdown--><p>在过去的七年里，我一直负责 freeCodeCamp 的基础设施。现在我确信，经验丰富的开发者可以在保持代码质量的同时提升 3-4 倍的编码速度。这就是 AI 辅助开发能够提供的优势。简单来说，使用像 GitHub Copilot 这样的 AI 工具作为你的编码伙伴可以提高效率。它们可以建议代码、帮助你调试以及加快处理重复性任务。</p>
<h3 id="">为什么这很重要</h3>
<p>传统编码时，你需要自己编写每一行代码、查找文档、弄清语法。有了 AI，你可以：</p>
<ul>
<li>
<p>专注于解决问题，而不是记住语法</p>
</li>
<li>
<p>通过实时看到优秀的代码示例更快地学习</p>
</li>
<li>
<p>快速构建项目而不牺牲质量</p>
</li>
</ul>
<p>对于有经验的开发者来说，在 AI 帮助下可以更快地完成任务。但关键是：<strong>你需要了解如何有效使用这些工具</strong>。你也需要有编程背景来做到这一点。</p>
<p>感兴趣吗？让我们深入了解这些已经风靡全球的 AI 编码工具。</p>
<h2 id="">目录</h2>
<ul>
<li>
<p><a href="#essential-ai-terminology">基础 AI 术语</a></p>
</li>
<li>
<p><a href="#when-to-use-ai-vs-when-to-code-yourself">何时使用 AI 以及何时自己编码</a></p>
</li>
<li>
<p><a href="#prerequisites">前提条件</a></p>
</li>
<li>
<p><a href="#your-complete-learning-journey">你的完整学习旅程</a></p>
</li>
<li>
<p><a href="#how-to-generate-your-first-ai-assisted-code">如何生成你的第一个 AI 辅助代码（快速入门）</a></p>
</li>
<li>
<p><a href="#stage-1">阶段 1：基础 – 开始 AI 编码</a></p>
</li>
<li>
<p><a href="#stage-2">阶段 2：高级 GitHub Copilot 功能</a></p>
</li>
<li>
<p><a href="#stage-3">阶段 3：基于 CLI 的 AI 代理（Claude Code &amp; Gemini）</a></p>
</li>
<li>
<p><a href="#stage-4">阶段 4：掌握 – 结合工具和高级工作流程</a></p>
</li>
<li>
<p><a href="#common-ai-issues">常见 AI 问题</a></p>
</li>
<li>
<p><a href="#whats-next-after-completing-all-stages">完成所有阶段后接下来做什么？</a></p>
</li>
<li>
<p><a href="#conclusion">结语</a></p>
</li>
</ul>
<h2 id="essential-ai-terminology">基础 AI 术语</h2>
<p>在开始之前，确保你了解以下关键术语：</p>
<ul>
<li>
<p><strong>Tokens（令牌）：</strong> 把令牌想象成“词片”——AI 阅读你的代码和文本的方式。每个字符、单词或符号都会使用令牌。免费版本会限制你可以使用的令牌数量。</p>
</li>
<li>
<p><strong>Context Window（上下文窗口）：</strong> AI 能一次性“记住”多少代码/对话。就像短期记忆，更大的窗口意味着对项目的更好理解。</p>
</li>
<li>
<p><strong>Hallucinations（幻觉）：</strong> AI 自信地建议错误信息——例如，捏造不存在的函数。务必验证 AI 的建议！</p>
</li>
<li>
<p><strong>Prompt（提示）：</strong> 你给 AI 的指令——评论、问题或请求，用以指导其生成的代码。</p>
</li>
</ul>
<h2 id="when-to-use-ai-vs-when-to-code-yourself">何时使用 AI 以及何时自己编码</h2>
<p><strong>使用 AI 的场合：</strong></p>
<ul>
<li>
<p>编写模板代码（getters、setters、基本的 CRUD）</p>
</li>
<li>
<p>学习新的框架或语法</p>
</li>
<li>
<p>编写测试和文档</p>
</li>
<li>
<p>重构重复模式</p>
</li>
<li>
<p>在语法错误上解开困境</p>
</li>
</ul>
<p><strong>自己编码的场合：</strong></p>
<ul>
<li>
<p>设计系统架构</p>
</li>
<li>
<p>做出关键的安全决策</p>
</li>
<li>
<p>编写复杂的业务逻辑</p>
</li>
<li>
<p>学习新概念（首次）</p>
</li>
<li>
<p>进行关键的性能优化</p>
</li>
</ul>
<p><strong>黄金法则：</strong> 使用 AI 加速实施，但自己做架构决策。AI 擅长“如何”，但你决定“什么”和“为什么”。</p>
<h2 id="prerequisites">前提条件</h2>
<p>在开始本教程之前，你应该具备：</p>
<ul>
<li>
<p><strong>基本编程经验</strong> – 你可以用某种语言编写简单程序</p>
</li>
<li>
<p><strong>安装有代码编辑器</strong> – 推荐使用 VS Code（从 <a href="http://code.visualstudio.com/">code.visualstudio.com</a> 免费获取）</p>
</li>
<li>
<p><strong>基本 Git 知识</strong> – 你知道如何提交和推送代码</p>
</li>
<li>
<p><strong>免费起步</strong> – 许多工具现在都有慷慨的免费版本，付费计划最低大约每月 10-20 美元</p>
</li>
</ul>
<h2 id="your-complete-learning-journey">你的完整学习旅程</h2>
<p>这篇全面的教程提供了一个循序渐进的计划，旨在助你成为 AI 辅助开发的专家：</p>
<p>注意：为了保持教程的易用性，我们将只关注一些核心工具。但你应该研究和探索更多适合你特定需求的工具，超出我们在此使用的工具。</p>
<h3 id="">学习路径</h3>
<p>你将经过 4 个阶段：掌握 GitHub Copilot 基础、解锁高级功能如聊天模式和代理、探索 CLI 工具（Claude Code 和 Gemini），最终战略性地结合多个工具实现完整的项目工作流程。</p>
<p>首先，让我们快速看看如何生成你的第一个 AI 代码片段。</p>
<h2 id="how-to-generate-your-first-ai-assisted-code">如何生成你的第一个 AI 辅助代码（快速入门）</h2>
<p>让我们从最基础的开始。别担心选择“完美”的工具——你总可以后续切换。以下是开始的方法：</p>
<h3 id="githubcopilot">GitHub Copilot（推荐初学者使用）</h3>
<p>你可以通过以下步骤安装 GitHub Copilot：</p>
<ol>
<li>
<p>打开 VS Code</p>
</li>
<li>
<p>点击扩展图标（或按 Ctrl+Shift+X）</p>
</li>
<li>
<p>搜索“GitHub Copilot”</p>
</li>
<li>
<p>点击“安装”</p>
</li>
<li>
<p>使用你的 GitHub 账户登录</p>
</li>
</ol>
<p><strong>提示：</strong> 学生、教师和开源软件维护者<a href="https://docs.github.com/en/copilot/how-tos/manage-your-account/getting-free-access-to-copilot-pro-as-a-student-teacher-or-maintainer">可以免费获得 Pro 计划</a>，这可以提供无限制的使用，而不是免费的使用限制。</p>
<h3 id="ai">你的第一个 AI 建议</h3>
<p>安装后，创建一个名为 <code>test.js</code> 的新文件，并输入：</p>
<pre><code>// function to calculate the area of a circle
</code></pre>
<p>按下 Enter 键等待。你会看到灰色文本出现——这是您的 AI 建议！按 Tab 键接受它。</p>
<p>就这样！您刚刚获得了第一个 AI 建议！是不是很酷？</p>
<h2 id="stage-1">阶段 1：基础 – 开始使用 AI 编码</h2>
<h3 id="1">第 1 步：了解您的选项</h3>
<p>将 AI 编码助手想象成不同类型的有帮助的朋友和同事。让我们来浏览几个：</p>
<p><strong>基于 IDE 的：</strong> 一些工具设计为可以与熟悉的代码编辑器一起工作，或作为编辑器如 VS Code 的独立分支。例如：</p>
<ul>
<li>
<p><strong>GitHub Copilot (VS Code 扩展)</strong> – 来自 GitHub 的 AI 编码助手，直接在 VS Code 中工作，提供 Tab 补全和聊天功能</p>
</li>
<li>
<p><strong>Cursor (独立)</strong> – VS Code 分支，具有增强的代理模式、更快的自主编码和更好的大代码库重构处理</p>
</li>
<li>
<p><strong>Windsurf (独立或 VS Code 扩展)</strong> – 专注于实时建议和团队功能的协作 AI 开发</p>
</li>
<li>
<p><strong>Zed</strong> – 高性能编辑器，内置 AI 辅助功能和快速渲染</p>
</li>
</ul>
<p><strong>基于 CLI 的：</strong> 一些工具是基于 CLI 的，您可以在终端应用中启动：</p>
<ul>
<li>
<p><strong>Claude Code</strong> – Anthropic 的终端 AI，用于自主开发会话和复杂推理</p>
</li>
<li>
<p><strong>Gemini</strong> – Google 的 CLI 工具，具有大上下文窗口和多模态功能（图像、文档）</p>
</li>
<li>
<p><strong>OpenCode</strong> – 开源替代方案，具有可自定义模型和本地处理选项</p>
</li>
<li>
<p><strong>Cursor CLI</strong> – Cursor 的终端版本，用于命令行 AI 辅助</p>
</li>
</ul>
<p><strong>基于 UI 和后台代理的工具：</strong> 除此之外，还有一些后台代理和工具可以完全在后台运行，例如执行拉取请求审查等。</p>
<p>例如，如果您设置了它们，ChatGPT 和 Claude 的桌面应用程序都可以编辑本地文件系统上的文件。同样，一些基于云的代理可以“在后台运行”以完成您的指令。我们将在本指南范围内排除这些。</p>
<h3 id="2tab">第 2 步：做出选择并学习自动建议（Tab 补全）</h3>
<p>对于您的第一阶段，我建议从 GitHub Copilot 开始。您可以在学习完基础知识后随时切换到适合您需求的工具。</p>
<h3 id="3">第 3 步：逐步设置</h3>
<h4 id="githubcopilot">如何设置 GitHub Copilot（如果您之前已经遵循过快速入门，可以跳过此步骤）</h4>
<ol>
<li>
<p><strong>打开 VS Code。</strong> 如果您没有，请从 <a href="https://code.visualstudio.com/">code.visualstudio.com</a> 下载。</p>
</li>
<li>
<p><strong>安装扩展</strong></p>
<ul>
<li>
<p>按 <code>Ctrl+Shift+X</code>（Windows/Linux）或 <code>Cmd+Shift+X</code>（Mac）</p>
</li>
<li>
<p>在搜索框中输入“GitHub Copilot”</p>
</li>
<li>
<p>点击蓝色的“安装”按钮</p>
</li>
<li>
<p>您会看到一个弹出窗口要求您登录</p>
</li>
</ul>
</li>
<li>
<p><strong>登录</strong></p>
<ul>
<li>
<p>点击“登录 GitHub”</p>
</li>
<li>
<p>您的浏览器会打开</p>
</li>
<li>
<p>使用您的 GitHub 帐户登录（如果需要，可以在 <a href="http://github.com/">github.com</a> 免费创建一个）</p>
</li>
<li>
<p>点击“授权 GitHub Copilot”</p>
</li>
</ul>
</li>
<li>
<p><strong>开始使用 Copilot</strong></p>
<ul>
<li>返回 VS Code，您会看到“GitHub Copilot 已准备好使用”</li>
</ul>
</li>
</ol>
<h3 id="4tab">第 4 步：掌握 Tab 补全</h3>
<p>让我们确保它正常工作。创建一个新文件：<code>hello.py</code>。输入此注释并按 Enter：</p>
<pre><code># function to greet a user by name
</code></pre>
<p>等待 1-2 秒。您应该会看到灰色文本出现。只需按 <code>Tab</code> 接受建议。</p>
<p><strong>您应该看到的内容：</strong></p>
<pre><code># function to greet a user by name
def greet_user(name):
    return f"Hello, {name}!"
</code></pre>
<p>如果您看到了这个，恭喜您！您现在正在使用 AI 帮助您编写代码。</p>
<p>如果您遇到设置问题，可以查看 <a href="http://localhost:3333/#troubleshooting-quick-reference">疑难解答快速参考</a> 寻求解决方案。</p>
<h3 id="5">第 5 步：基本键盘快捷键和第一次练习</h3>
<p>以下是您第一周需要的唯一快捷键：</p>
<p><strong>基础：</strong></p>
<ul>
<li>
<p><code>Tab</code> – 接受 AI 建议（这个用得最多！）</p>
</li>
<li>
<p><code>Esc</code> – 拒绝建议（当您不需要时）</p>
</li>
</ul>
<p>当您准备好了解更多时，尝试这些：</p>
<p><strong>Windows/Linux：</strong></p>
<ul>
<li>
<p><code>Alt+]</code> – 查看下一个建议</p>
</li>
<li>
<p><code>Alt+[</code> – 查看上一个建议</p>
</li>
<li>
<p><code>Ctrl+Enter</code> – 在面板中查看所有建议</p>
</li>
</ul>
<p><strong>macOS：</strong></p>
<ul>
<li>
<p><code>Option+]</code> （或 <code>Alt+]</code>） – 查看下一个建议</p>
</li>
<li>
<p><code>Option+[</code> （或 <code>Alt+[</code>) – 查看上一个建议</p>
</li>
<li>
<p><code>Ctrl+Enter</code> – 在面板中查看所有建议</p>
</li>
</ul>
<h3 id="1">阶段 1 实践练习</h3>
<h4 id="">练习：构建一个简单的待办事项应用</h4>
<ol>
<li>
<p>创建一个名为 <code>todo.js</code> 的新文件</p>
</li>
<li>
<p>以此注释开始：<code>// TODO app with add, remove, and list functions</code></p>
</li>
<li>
<p>添加此注释并等待 AI 建议：<code>// function to add a new todo item</code></p>
</li>
<li>
<p>如果建议看起来不错，按 Tab 键接受</p>
</li>
<li>
<p>继续为移除和列出函数添加注释</p>
</li>
<li>
<p>测试您的函数以确保它们工作正常</p>
</li>
</ol>
<p><strong>目标：</strong> 学习通过清晰的注释与 AI “对话”，并建立接受/拒绝建议的信心。</p>
<h3 id="">准备好进入下一阶段了吗？在继续之前，请确保您能够：</h3>
<pre><code>- [ ] 通过输入注释获取 AI 建议
- [ ] 使用 Tab 接受建议，使用 Esc 拒绝建议
- [ ] 使用 Alt+] 和 Alt+[ 查看不同的建议
- [ ] 在 AI 帮助下编写基本函数
</code></pre>
<p>如果您对这些基本内容感到满意，就可以学习更强大的 Copilot 功能了。</p>
<h2 id="stage-2">阶段 2：高级 GitHub Copilot 功能</h2>
<h3 id="6ai">第 6 步：获取更好的 AI 建议</h3>
<p>现在您已经了解了基础知识，让我们学习如何从您的 AI 那里得到_更好_的建议。诀窍在于了解您的 AI 能看到什么。</p>
<h4 id="ai">您的 AI 助手能看到什么</h4>
<p>想象您的 AI 助手就像一个站在你肩头、随时提供帮助的朋友。它能看到：</p>
<ol>
<li>
<p><strong>您当前正在输入的内容</strong>——您的当前文件</p>
</li>
<li>
<p><strong>其他打开的标签页</strong>——您打开的文件（这很重要！）</p>
</li>
<li>
<p><strong>您的项目结构</strong>——文件夹和文件名</p>
</li>
<li>
<p><strong>您的注释</strong>——这是您与 AI “对话” 的方式</p>
</li>
</ol>
<h4 id="">“相邻标签页”技巧</h4>
<p>这是一个能为您节省数小时的专业提示：<strong>在标签页中打开相关文件</strong>。</p>
<p><strong>示例：</strong> 如果您在编写一个 React 组件：</p>
<ul>
<li>
<p>打开您的组件文件 (<code>Button.jsx</code>)</p>
</li>
<li>
<p>还要打开 CSS 文件 (<code>Button.css</code>)</p>
</li>
<li>
<p>同时也让测试文件可见 (<code>Button.test.js</code>)</p>
</li>
</ul>
<p>然后，您可以通过几种方式与 AI 共享这些额外文件的上下文：</p>
<ul>
<li>
<p><strong>@提及文件：</strong> 在聊天中键入 <code>@filename.js</code> 以引用特定文件</p>
</li>
<li>
<p><strong>使用 @workspace：</strong> 这个聊天参与者可以看到您项目中的所有文件</p>
</li>
<li>
<p><strong>拖放：</strong> 只需将文件从资源管理器窗口拖动到聊天窗口</p>
</li>
<li>
<p><strong>选择代码：</strong> 高亮代码并右键单击 “Ask Copilot” 以将其包括在上下文中</p>
</li>
</ul>
<p>AI 使用这些打开的文件来理解您的项目结构，并建议更相关的代码以匹配您现有的模式。</p>
<h3 id="7">第 7 步：质量控制与最佳实践</h3>
<h4 id="ai">了解 AI 的局限性</h4>
<p>AI 功能强大，但并不完美。以下是需要注意的关键事项。</p>
<p><strong>常见的 AI 错误：</strong></p>
<ol>
<li>
<p>虚构的函数：例如，<code>const result = array.superSort();</code> 并不存在！</p>
</li>
<li>
<p>错误的参数：例如，当函数期望 <code>greetUser(name)</code> 时，<code>greetUser("John", "Doe");</code></p>
</li>
<li>
<p>过于复杂的解决方案：例如，<code>const isEven = (num) =&gt; num.toString(2).slice(-1) === "0";</code> - 只需使用 <code>num % 2 === 0</code></p>
</li>
</ol>
<p>快速质量检查清单：</p>
<pre><code>- [ ] 测试代码 - 它真的有效吗？
- [ ] 阅读 - 逻辑上是否合理？
- [ ] 检查基础 - 所有函数/变量是否已定义？
- [ ] 信任直觉 - 如果感觉不对，进行调查
</code></pre>
<h4 id="">安全要点</h4>
<p>在接受 AI 建议之前，请确保检查这些安全问题：</p>
<pre><code>- [ ] 没有硬编码的密码或 API 密钥
- [ ] 用户输入已验证
- [ ] 没有对用户数据使用 eval()
- [ ] 错误消息不暴露敏感信息
</code></pre>
<h4 id="">更好的提示写作</h4>
<p>这里有一个编写可靠提示的公式：是什么 + 如何 + 返回类型。</p>
<pre><code>// ❌ 含糊： "make function"
// ✅ 清晰： "function to validate email format using regex, returns boolean"
</code></pre>
<h4 id="copilot">使用 Copilot 指令进行仓库级自定义</h4>
<p>GitHub Copilot 现在支持通过 <code>.github/copilot-instructions.md</code> 文件进行仓库级别的自定义。此功能帮助 Copilot 理解您项目的特定模式和约定。</p>
<p>以下是设置 Copilot 指令的方法：</p>
<pre><code># 如果不存在，创建 GitHub 目录
mkdir -p .github
touch .github/copilot-instructions.md
</code></pre>
<p>示例 <a href="http://copilot-instructions.md/">copilot-instructions.md</a> 文件：</p>
<pre><code># Copilot 指令

## 代码风格

- 使用具有 hooks 的 React 函数组件
- 为新文件更偏好使用 TypeScript 而不是 JavaScript
- 使用 Tailwind CSS 进行样式设计
- 遵循 `/src/components` 中的现有文件结构

## 测试

- 使用 React Testing Library 编写测试
- 将测试文件放在 `__tests__` 目录中
- 使用描述性测试名称来解释行为

## API 模式

- 使用自定义钩子进行 API 调用
- 一致地处理加载和错误状态
- 使用 React Query 进行数据提取

## 命名约定

- 组件：PascalCase（例如，`UserProfile.tsx`）
- 钩子：以“use”开头的 camelCase（例如，`useUserData.ts`）
- 工具：camelCase（例如，`formatDate.ts`）
</code></pre>
<p><strong>这项功能实现了：</strong></p>
<ul>
<li>
<p>Copilot 建议符合您项目模式的代码</p>
</li>
<li>
<p>自动遵循您的命名约定</p>
</li>
<li>
<p>建议合适的测试方法</p>
</li>
<li>
<p>理解您偏好的库和框架</p>
</li>
</ul>
<p><strong>最佳实践：</strong></p>
<ul>
<li>
<p>保持指令清晰和具体</p>
</li>
<li>
<p>随着项目标准的发展及时更新</p>
</li>
<li>
<p>包括首选模式的示例</p>
</li>
<li>
<p>提到您使用的库和框架</p>
</li>
</ul>
<h3 id="8copilot">第 8 步：解锁高级 Copilot 功能</h3>
<h4 id="">了解您的选择</h4>
<p>GitHub Copilot 提供多种方式来获得 AI 帮助：</p>
<ol>
<li>
<p><strong>Tab 补全</strong>（您一直在使用的）——在输入时提供建议</p>
</li>
<li>
<p><strong>聊天模式</strong>——与 AI 就您的代码进行对话</p>
</li>
<li>
<p><strong>编辑模式</strong>——请求 AI 对现有代码进行修改</p>
</li>
<li>
<p><strong>代理模式</strong>——让 AI 自主完成大型任务</p>
</li>
</ol>
<h4 id="">模型选择</h4>
<p>Copilot 现在提供不同的 AI 模型以满足不同需求：</p>
<p>订阅免费：</p>
<ul>
<li>
<p><strong>GPT-4.1</strong> – 默认模型，综合性能强大</p>
</li>
<li>
<p><strong>GPT-4</strong> – 对大多数编码任务来说可靠</p>
</li>
</ul>
<p>高级模型（有限的每月使用量）：</p>
<ul>
<li>
<p><strong>Claude 3.5 Sonnet</strong> – 适合复杂逻辑</p>
</li>
<li>
<p><strong>GPT-5</strong> – 最新且最强大</p>
</li>
<li>
<p><strong>Gemini 2.0 Flash</strong> – 响应非常快速</p>
</li>
</ul>
<p><strong>如何切换模型：</strong> 点击聊天视图中的模型下拉菜单</p>
<p><strong>提示：</strong> 学习时从免费模型（GPT-4.1）开始，对于复杂问题保留高级模型使用。</p>
<h4 id="githubcopilot">GitHub Copilot 的局限性</h4>
<p>以下是在使用 AI 来帮助您进行编码时需要考虑的一些重要事项：</p>
<ul>
<li>
<p><strong>互联网依赖性</strong> – 需要稳定的连接以获得建议</p>
</li>
<li>
<p><strong>上下文限制</strong> – 只能看到打开的文件，而不是整个项目结构</p>
</li>
<li>
<p><strong>免费账号限制</strong> – 每月 2,000 次补全和 50 次聊天请求</p>
</li>
<li>
<p><strong>代码质量参差不齐</strong> – 始终审查建议，特别是涉及安全性敏感的代码时</p>
</li>
<li>
<p><strong>学习曲线</strong> – 编写有效的提示需要时间，特别是对于复杂任务</p>
</li>
<li>
<p><strong>隐私考虑</strong> – 您的代码会被发送到 GitHub 的服务器（请检查您的组织的政策）</p>
</li>
</ul>
<h4 id="">基础聊天与建议</h4>
<p>您可能想知道 - 什么时候应该使用 Tab 补全，什么时候应该使用聊天？最好是用 Tab 补全来编写新函数、快速的语法帮助和模式补全。可以使用聊天来解释现有代码、获取错误帮助和规划解决问题的方法。</p>
<p><strong>尝试一下：</strong> 打开聊天（Ctrl+Shift+I）并询问：“这个函数有什么作用？”同时选择代码。</p>
<h3 id="9">第9步：掌握聊天和代理模式</h3>
<h4 id="">三种聊天模式</h4>
<ol>
<li><strong>询问模式（默认）</strong> – 用于问题和解释：</li>
</ol>
<pre><code>“这个函数有什么作用？”
“如何优化这段代码？”
“解释此错误消息”
</code></pre>
<ol start="2">
<li><strong>编辑模式</strong> – 用于对现有代码进行更改：</li>
</ol>
<pre><code>“重构这段代码以使用 async/await”
“为所有 API 调用添加错误处理”
“将其转换为 TypeScript”
</code></pre>
<ul>
<li>
<p>显示内联差异后应用更改</p>
</li>
<li>
<p>可跨多个文件工作</p>
</li>
<li>
<p>适合系统化重构</p>
</li>
</ul>
<ol start="3">
<li><strong>代理模式</strong> – 用于自主开发：</li>
</ol>
<pre><code>“创建一个带身份验证的 REST API”
“使用React和测试构建一个待办应用”
“将此代码库从Vue 2迁移到Vue 3”
</code></pre>
<ul>
<li>
<p>按 <code>Cmd+Shift+I</code> （Mac）或 <code>Ctrl+Shift+Alt+I</code>（Linux）或 <code>Ctrl+Shift+I</code>（Windows）</p>
</li>
<li>
<p>可独立工作数小时</p>
</li>
<li>
<p>自动安装包、创建文件、运行测试</p>
</li>
</ul>
<h4 id="">何时使用各模式</h4>
<p>每种模式都有特定的使用场景。当您学习新概念时，希望理解现有代码、获取解释和去计划方法时，使用询问模式。</p>
<p>当您正在重构现有代码、应用一致的更改、为现有功能添加特性或进行样式/模式更新时，使用编辑模式。</p>
<p>代理模式在构建完整特性（30分钟以上的工作）、设置新项目、大规模重构时有用，并且在您希望在 AI 编码时处理其他事情时使用。</p>
<h4 id="">代理模式示例</h4>
<p>小型代理任务（15分钟）：</p>
<pre><code>“为我的 Express 应用添加用户身份验证”
</code></pre>
<p>代理生成的内容：</p>
<pre><code>// middleware/auth.js
const jwt = require('jsonwebtoken');

const authenticateToken = (req, res, next) =&gt; {
  const authHeader = req.headers['authorization'];
  const token = authHeader &amp;&amp; authHeader.split(' ')[1];

  if (!token) return res.sendStatus(401);

  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) =&gt; {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
};

// routes/auth.js
router.post('/login', async (req, res) =&gt; {
  // 使用 bcrypt 进行身份验证的逻辑
  const accessToken = jwt.sign({username: user.username}, process.env.ACCESS_TOKEN_SECRET);
  res.json({accessToken: accessToken});
});
</code></pre>
<p><strong>我发现的关键问题：</strong> 代理最初忘记对密码进行哈希处理，并且没有包括刷新令牌。这需要一次迭代来修复安全漏洞并添加适当的错误处理。</p>
<p>大型代理任务（4小时以上）：</p>
<pre><code>“将此基于类的 React 应用更新为使用 TypeScript 的 hooks”
</code></pre>
<p>代理生成的内容：</p>
<pre><code>// 之前（类组件）
class UserProfile extends React.Component {
  constructor(props) {
    this.state = { user: null, loading: true };
  }
  // ... 生命周期方法
}

// 之后（Hooks + TypeScript）
interface User {
  id: number;
  name: string;
  email: string;
}

const UserProfile: React.FC = () =&gt; {
  const [user, setUser] = useState&lt;User | null&gt;(null);
  const [loading, setLoading] = useState(true);

  useEffect(() =&gt; {
    fetchUser().then(setUser).finally(() =&gt; setLoading(false));
  }, []);

  return &lt;div&gt;{loading ? 'Loading...' : user?.name}&lt;/div&gt;;
};
</code></pre>
<p>**我发现的关键问题：**代理成功更新了47个文件，但最初在事件处理程序中存在类型问题，并且需要改进泛型类型。自动化测试还需要人工检查以确保适当的 TypeScript 覆盖。</p>
<p>Chat 参与者是专门的 AI 助手，可以访问您的开发环境中的特定部分。可以将它们视为不同领域的专家，能够帮助完成有针对性的任务。</p>
<p>它们基本上是以 <code>@</code> 作为前缀的 AI 助手，具有特殊的知识和能力：</p>
<ul>
<li>
<p><strong>@workspace</strong> 可以访问您整个项目的结构，可以搜索文件并理解组件之间的关系。当您需要项目范围的分析时，请使用 <code>@workspace</code>："找出此项目中的所有 API 端点" 或 "显示用户身份验证是在哪里实现的。"</p>
</li>
<li>
<p><strong>@terminal</strong> 了解命令行操作，可以建议 shell 命令并解释终端输出。对于命令行帮助，请使用 <code>@terminal</code>："哪个命令可以运行测试？" 或 "如何为生产构建此项目？"</p>
</li>
<li>
<p><strong>@vscode</strong> 是 VS Code 功能方面的专家，可以帮助进行设置、调试和编辑器配置。对于编辑器帮助，请使用 <code>@vscode</code>："为 Node.js 设置调试" 或 "为此项目配置自动格式化。"</p>
</li>
</ul>
<p><strong>示例用法:</strong></p>
<pre><code>@workspace 你能找到这个项目中的所有数据库模型吗？
@terminal 安装依赖项并启动开发服务器的命令是什么？
@vscode 如何设置断点以调试这个 Express 应用？
</code></pre>
<h3 id="10">步骤 10：高级用户功能和高级工作流程</h3>
<p>除了您已经了解的 Copilot 核心功能外，还有一些专门的工具和命令可以极大地提高您的生产力。这些功能超越基本的聊天模式和模型选择，专注于复杂的多文件操作和高级自动化。</p>
<h4 id="">高级斜线命令</h4>
<pre><code>/doc - 生成文档
/explain - 详细的代码说明
/fix - 修复选定代码中的错误
/tests - 生成单元测试
/new - 创建新的项目结构
</code></pre>
<h4 id="">多文件操作</h4>
<p><strong>使用 # 引用：</strong></p>
<p><code>#</code> 符号创建特定的引用，告诉 Copilot 精确关注哪里。这些引用就像是指向项目不同部分的精准指针：</p>
<ul>
<li>
<p><strong>#file:filename</strong>: 引用特定文件：<code>#file:UserModel.js</code></p>
</li>
<li>
<p><strong>#codebase</strong>: 引用整个项目代码库以进行搜索</p>
</li>
<li>
<p><strong>#selection</strong>: 引用当前选定的代码</p>
</li>
<li>
<p><strong>#editor</strong>: 引用当前活动文件</p>
</li>
</ul>
<pre><code>"更新 #file:UserModel.js 以包含时间戳"
"在 #codebase 中搜索所有数据库查询"  
"将 #selection 重构为现代 JavaScript 语法"
"为所有 API 调用添加错误处理到 #editor"
</code></pre>
<p>这些引用帮助 Copilot 准确理解要查看和更改的位置，使多文件操作更加精确。</p>
<p><strong>拖放操作：</strong></p>
<p>拖放是提供给 Copilot 的最直观的上下文方式之一。您可以将文件从 VS Code 浏览器中直接拖入聊天窗口，Copilot 会立即了解其内容和结构。</p>
<p>当您处理相关组件并需要 AI 理解不同文件如何相互连接时，此功能特别有用。Copilot 会在整个对话中记住这些文件关系，因此在继续相同的讨论时无需再次上传文件。</p>
<p>这种上下文保留在多个聊天会话中有效，使您可以轻松在复杂的多文件项目中继续之前的工作。</p>
<h3 id="2">阶段 2 实践练习</h3>
<h4 id="1">练习 1：聊天模式实践</h4>
<ol>
<li>
<p>使用提问模式理解一个复杂的函数</p>
</li>
<li>
<p>切换到编辑模式进行重构</p>
</li>
<li>
<p>比较不同的方法</p>
</li>
</ol>
<h4 id="2">练习 2：代理模式项目</h4>
<ol>
<li>
<p>启动代理模式 (<code>Shift+Cmd+I</code>)</p>
</li>
<li>
<p>请求："创建一个带测试的简单待办应用"</p>
</li>
<li>
<p>观察自主开发过程</p>
</li>
<li>
<p>查看生成的代码</p>
</li>
</ol>
<h4 id="3">练习 3：高级功能</h4>
<ol>
<li>
<p>使用 @ 参与者进行项目问题</p>
</li>
<li>
<p>试验斜线命令</p>
</li>
<li>
<p>练习多文件操作</p>
</li>
</ol>
<h3 id="cli">准备好使用 CLI 工具了吗？</h3>
<p>您现在已经学习了在 VS Code 中使用 GitHub Copilot 的基础知识！像 Claude Code 和 Gemini 这样的 CLI 工具为基于终端的开发提供了更强大的功能。</p>
<p>如果你对终端 AI 感兴趣，可以继续学习下方的第三阶段。如果你更愿意继续使用 VS Code，请跳到第四阶段以获取高级工作流程。</p>
<h2 id="stage-3">阶段 3：基于 CLI 的 AI 代理（Claude Code &amp; Gemini）</h2>
<h3 id="11claudecodeai">步骤 11：认识 Claude Code——您的终端 AI 助手</h3>
<h4 id="claudecode">什么是 Claude Code？</h4>
<p>还记得 GitHub Copilot 如何在 VS Code 中帮助你吗？Claude Code 在终端中为你提供相同的帮助。</p>
<p>不用在 VS Code 中键入和获取建议，你可以在终端中键入并与 AI 对话。这就像在命令行上有一个编程伙伴。</p>
<h4 id="">简单例子：</h4>
<p>在 VS Code 中使用 Copilot：</p>
<pre><code>// 创建一个函数来验证电子邮件
[AI 提供建议代码]
</code></pre>
<p>在终端中使用 Claude Code：</p>
<pre><code>claude
&gt; 创造一个函数来验证电子邮件地址
[AI 为你编写代码]
</code></pre>
<p>那么，什么时候应该使用 VS Code/Copilot，什么时候应该使用 Claude Code 呢？</p>
<p><strong>如果你：</strong></p>
<ul>
<li>
<p>喜欢在终端工作</p>
</li>
<li>
<p>想要与 AI 进行关于代码的对话</p>
</li>
<li>
<p>需要有关命令行任务的帮助</p>
</li>
<li>
<p>想要更多控制 AI 互动</p>
</li>
</ul>
<p>Claude Code 非常适合你。</p>
<ul>
<li>
<p>偏好可视化编辑器</p>
</li>
<li>
<p>对当前的工作流程感到满意</p>
</li>
<li>
<p>不花太多时间在终端</p>
</li>
</ul>
<h4 id="">定价</h4>
<p>Claude Code 需要 Claude Pro（每月 20 美元）或 ClaudeMax（每月 100 美元）订阅，或使用 API 积分按使用量付费。</p>
<h4 id="claudecode">Claude Code 限制</h4>
<p>如果您计划使用 Claude Code，请考虑以下重要事项：</p>
<ul>
<li>
<p><strong>仅付费</strong> – 没有免费版本，需要 Claude Pro 订阅或 API 积分</p>
</li>
<li>
<p><strong>基于终端</strong> – 不如 IDE 集成工具可视化</p>
</li>
<li>
<p><strong>学习曲线</strong> – 需要熟悉命令行界面</p>
</li>
<li>
<p><strong>上下文管理</strong> – 需要手动管理对话上下文</p>
</li>
<li>
<p><strong>依赖互联网</strong> – 所有操作都需要稳定的连接</p>
</li>
<li>
<p><strong>会话限制</strong> – 长时间的自主会话会消耗大量 API 积分</p>
</li>
</ul>
<h4 id="">安装</h4>
<p>推荐（所有平台）：</p>
<pre><code>npm install -g @anthropic-ai/claude-code
</code></pre>
<p>其他安装方式：</p>
<ul>
<li>
<p><strong>macOS/Linux</strong>: <code>curl -fsSL https://claude.ai/install.sh | bash</code></p>
</li>
<li>
<p><strong>Windows</strong>: <code>irm https://claude.ai/install.ps1 | iex</code></p>
</li>
</ul>
<h4 id="">基本用法</h4>
<p><strong>交互模式（推荐）：</strong></p>
<p>交互模式是 Claude Code 的主要界面，您可以与 AI 实时对话。不同于一次性命令，交互模式创建了一个持久的会话，您可以提出后续问题、迭代解决方案，并随着时间的推移构建复杂的项目。</p>
<p>推荐使用交互模式，因为：</p>
<ul>
<li>
<p><strong>上下文持久性</strong>：Claude 会记住整个对话和项目上下文</p>
</li>
<li>
<p><strong>迭代开发</strong>：您可以优化请求，并基于先前的回应进行改进</p>
</li>
<li>
<p><strong>实时协作</strong>：在工作时提问、获取解释并修改方法</p>
</li>
<li>
<p><strong>会话恢复</strong>：使用 <code>claude --resume</code> 继续之前的对话</p>
</li>
</ul>
<p><strong>其他可用模式：</strong></p>
<ul>
<li>
<p><strong>一次性模式</strong>：单个命令执行（下文说明）</p>
</li>
<li>
<p><strong>代理模式</strong>：可独立工作数小时的自主开发会话</p>
</li>
</ul>
<ol>
<li>导航到您的项目：</li>
</ol>
<pre><code>cd your-project
claude
</code></pre>
<ol start="2">
<li>自然地开始对话：</li>
</ol>
<pre><code>Claude Code &gt; 分析这个代码库并提出改进建议

Claude Code &gt; 现在帮我重构用户认证

Claude Code &gt; 为支付模块添加单元测试
</code></pre>
<ol start="3">
<li>继续之前的会话：</li>
</ol>
<pre><code>claude --resume
</code></pre>
<p><strong>一次性命令（用于快速任务）：</strong></p>
<p>一次性命令是执行特定任务后即退出的单次执行命令。与交互模式不同，这些命令不保留对话上下文 —— 非常适合快速、独立的任务。</p>
<p><strong>什么是一次性命令？</strong></p>
<p>这些是在您的终端中直接运行带有特定指令的命令，而不进入交互会话。Claude 将执行请求并立即提供结果。</p>
<p><strong>何时使用一次性命令：</strong></p>
<ul>
<li>
<p>快速分析或代码审查</p>
</li>
<li>
<p>简单文件修改</p>
</li>
<li>
<p>自动化脚本和 CI/CD 集成</p>
</li>
<li>
<p>需要单一特定答案时</p>
</li>
</ul>
<p><strong>示例：</strong></p>
<pre><code>claude "分析这个代码库并提出改进建议"
claude "修复 src/ 中的所有 TypeScript 错误"
claude "为 utils.js 生成单元测试"
claude "解释这个函数的作用" --file src/auth.js
</code></pre>
<p>关键区别在于一次性命令在运行之间不记住上下文，而交互模式保持完整的对话历史记录和项目理解。</p>
<p><strong>交互式 vs 自主会话：</strong></p>
<p>在交互模式内，您可以选择协作和自主的方法：</p>
<p><strong>交互式会话（协作）：</strong></p>
<pre><code>Claude Code &gt; 我正在构建用户认证。我们应该采用什么方法？

你：使用 JWT 令牌和刷新令牌轮换

Claude Code &gt; 实现带刷新令牌的 JWT 认证
[逐步向您展示实现方案]

Claude Code &gt; 我还需要添加密码重置功能吗？

你：是的，使用基于邮件的重置
</code></pre>
<p><strong>自主会话（免动手开发）：</strong></p>
<pre><code>Claude Code &gt; 构建一个完整的用户管理系统，包括认证、个人资料、偏好设置和管理功能。使用安全和测试的最佳实践。

[Claude 自主工作数小时，定期提供更新]
[最终结果：完整的用户管理系统，准备投入生产]
</code></pre>
<p><strong>何时使用每种模式：</strong> 在学习时或希望对决策进行控制时使用交互式会话。对于定义明确的任务，在您信任 Claude 能够独立做出良好选择时使用自主会话。</p>
<h4 id="">关键特性</h4>
<p><strong>思维模式（在交互式会话中使用）：</strong></p>
<p>思维模式是特定的命令，可以告诉 Claude 在响应前要进行多深的分析。您可以根据问题的复杂程度手动选择这些模式。</p>
<p><strong>何时使用每种模式：</strong></p>
<ul>
<li>
<p><code>think</code> – 对简单任务进行快速分析："think: review this function for bugs"</p>
</li>
<li>
<p><code>think hard</code> – 对复杂逻辑进行深入推理："think hard: optimize this algorithm"</p>
</li>
<li>
<p><code>think harder</code> – 复杂问题解决，考虑多重因素："think harder: design a scalable database schema"</p>
</li>
<li>
<p><code>ultrathink</code> – 对架构决策进行最大深度分析："ultrathink: evaluate microservices vs monolith for this project"</p>
</li>
</ul>
<p>Claude展示其通过较长的思考模式进行推理的过程。您将在得出最终答案之前看到逐步分析。更高的思考模式花费更多时间，但会提供更全面的解决方案。</p>
<p><strong>选择正确的模式：</strong></p>
<p>使用<code>think</code>进行快速代码审查，使用<code>think hard</code>调试复杂问题，使用<code>think harder</code>处理系统设计问题，使用<code>ultrathink</code>进行影响整个项目的重大架构决策。</p>
<h4 id="claudemd">使用Claude.md进行项目级定制</h4>
<p>Claude Code最强大的功能之一是使用<code>.claude/CLAUDE.md</code>文件进行项目级定制。这让你可以为Claude提供有关特定项目、编码标准和偏好的上下文。</p>
<p>像这样设置CLAUDE.md：</p>
<pre><code># 创建项目级配置
mkdir -p .claude
touch .claude/CLAUDE.md
</code></pre>
<p>以下是一个CLAUDE.md文件的示例：</p>
<pre><code># 项目上下文

这是一个使用Express和PostgreSQL的Node.js REST API。

## 编码标准

- 使用async/await，绝不使用回调
- 所有数据库查询使用Prisma ORM
- 为所有新功能使用Jest编写测试
- 遵循RESTful约定

## 项目结构

- `/src/routes` - API端点
- `/src/models` - 数据库模型
- `/src/middleware` - Express中间件
- `/tests` - 单元和集成测试

## 偏好

- 为所有新文件使用TypeScript
- 更喜欢函数式编程模式
- 为所有函数包含JSDoc注释
</code></pre>
<p>这有助于Claude自动理解您的项目结构。还帮助Claude遵循您的特定编码标准，为您的技术栈建议合适的模式，并在多次会话中记住您的偏好。</p>
<p><strong>专业提示：</strong> 随着项目的发展更新你的CLAUDE.md，以保持Claude的建议相关和准确。</p>
<h4 id="claudecode">Claude Code自主开发会话</h4>
<p>这是一个多小时的自主开发会话示例：</p>
<pre><code>cd your-project
claude
</code></pre>
<p><strong>你的提示：</strong>“构建一个可扩展的具有实时订阅、身份验证、速率限制和全面测试的GraphQL API”</p>
<p>Claude的6小时自主过程：</p>
<pre><code>Claude Code &gt; ultrathink: 分析需求并设计架构

[创建详细的技术规格和开发计划]

Claude Code &gt; 实现核心GraphQL服务器与Apollo
[运行：npm install apollo-server-express graphql]
[创建：server.js, schema/typeDefs.js, resolvers/index.js]

Claude Code &gt; 添加身份验证和授权中间件
[创建：middleware/auth.js, utils/jwt.js, models/User.js]
[实现具有适当错误处理的JWT策略]

Claude Code &gt; 设置具有Redis的实时订阅
[运行：npm install graphql-redis-subscriptions]
[配置WebSocket连接，创建订阅解析器]

Claude Code &gt; 实施速率限制和安全措施
[添加helmet, express-rate-limit, 查询复杂度分析]

Claude Code &gt; 编写全面的测试套件
[创建单元测试、集成测试、负载测试]
[运行：npm test - 看到8个失败的测试]

Claude Code &gt; 自动修复测试失败
[识别异步时间问题，修复解析器逻辑]
[反复重新运行测试直到94/94个测试通过]

Claude Code &gt; 性能优化和缓存
[实现Redis缓存，优化数据库查询]
[添加性能监控和日志记录]

Claude Code &gt; 生成全面的文档
[创建带示例的API文档，README，部署指南]

Claude Code &gt; 最终验证和清理
[运行完整的测试套件，检查代码覆盖率：96%]
[创建生产构建和部署脚本]
</code></pre>
<p>这代表了6小时的自主工作（在此期间你可以处理其他项目）。结果是一个生产就绪的GraphQL API，具有身份验证、实时功能和全面的测试。</p>
<p>为什么这有效：</p>
<ul>
<li>
<p><strong>自主反馈循环：</strong> Claude运行测试，看到失败，自动修复</p>
</li>
<li>
<p><strong>上下文感知：</strong> 保持对整个项目结构的理解</p>
</li>
<li>
<p><strong>自我纠正：</strong> 反复迭代解决方案直至正确工作</p>
</li>
<li>
<p><strong>工具集成：</strong> 无缝使用git, npm, 测试框架</p>
</li>
</ul>
<p><strong>网络搜索集成：</strong></p>
<p>Claude Code可以搜索网络以获取当前信息，这对于AI训练数据有截断日期特别有用。该功能帮助您保持与最新文档、最佳实践和解决方案的同步。</p>
<pre><code>Claude Code &gt; 搜索最新的React 19功能并更新我的组件

[Claude搜索网络，然后以发现继续对话]

Claude Code &gt; 现在将这些新功能应用于UserProfile组件
</code></pre>
<p><strong>网络搜索何时有帮助：</strong></p>
<ul>
<li>
<p>获取新库版本的当前文档</p>
</li>
<li>
<p>找到最近错误消息或漏洞的解决方案</p>
</li>
<li>
<p>研究最新的最佳实践和模式</p>
</li>
<li>
<p>比较当前问题的解决方案</p>
</li>
</ul>
<p>当Claude检测到需要当前信息时，网络搜索会自动发生，或者你可以通过在提示中提到“搜索”或“最新”来显式请求。</p>
<h4 id="claudecode">Claude Code键盘快捷键</h4>
<p>你可以使用这些键盘快捷键提高工作效率：</p>
<ul>
<li>
<p><code>Ctrl+C</code> – 取消当前输入或生成</p>
</li>
<li>
<p><code>Ctrl+D</code> – 退出 Claude Code 会话</p>
</li>
<li>
<p><code>Ctrl+L</code> – 清除终端屏幕</p>
</li>
<li>
<p><code>Up/Down arrows</code> – 导航命令历史</p>
</li>
<li>
<p><code>Esc</code> + <code>Esc</code> – 编辑上一条消息</p>
</li>
</ul>
<p><strong>多行输入：</strong></p>
<ul>
<li>
<p><code>\</code> + <code>Enter</code> – 快速转义以创建新行（适用于所有终端）</p>
</li>
<li>
<p><code>Option+Enter</code> (Mac) / <code>Shift+Enter</code> (已配置) – 插入新行</p>
</li>
</ul>
<h3 id="12googlegemini">步骤12：Google Gemini 命令行界面</h3>
<h4 id="geminiclaudecode">何时使用 Gemini 与 Claude Code：</h4>
<p>Gemini 是另一种基于命令行界面的 AI 工具，它与 Claude Code 相辅相成，而不是竞争。尽管 Claude Code 擅长深度推理和复杂的开发任务，但 Gemini 提供了独特的优势：大规模上下文窗口（超过100万个标记）、慷慨的免费限制以及强大的多模态能力。</p>
<p><strong>使用 Gemini，当您：</strong></p>
<ul>
<li>
<p>需要一次分析整个大型代码库</p>
</li>
<li>
<p>希望处理图像、图表或草图</p>
</li>
<li>
<p>在预算限制内工作（慷慨的免费套餐）</p>
</li>
<li>
<p>需要极大的上下文窗口来处理复杂项目</p>
</li>
</ul>
<p><strong>使用 Claude Code，当您：</strong></p>
<ul>
<li>
<p>需要复杂的推理和问题解决</p>
</li>
<li>
<p>希望进行自主开发会话</p>
</li>
<li>
<p>偏爱用于复杂分析的高级思维模式</p>
</li>
<li>
<p>正在构建需要详细规划的生产系统</p>
</li>
</ul>
<p><strong>最佳方法：</strong> 许多开发人员策略性地使用这两种工具——Gemini用于分析和视觉输入，Claude Code用于复杂的开发任务。</p>
<p>Gemini 将 Google 的 AI 带到您的终端，并提供慷慨的免费限制。</p>
<h4 id="">安装</h4>
<p>使用 npx (推荐试用)：</p>
<pre><code>npx @google/gemini-cli
</code></pre>
<p>全局安装：</p>
<pre><code>npm install -g @google/gemini-cli
gemini  # 开始交互式会话
</code></pre>
<h4 id="">认证</h4>
<ol>
<li>使用 Google 登录：</li>
</ol>
<pre><code>gemini auth login
</code></pre>
<ol start="2">
<li>检查状态：</li>
</ol>
<pre><code>gemini auth status
</code></pre>
<p>免费限制：</p>
<ul>
<li>
<p>60 次请求/分钟</p>
</li>
<li>
<p>使用 Google 帐户每日 1,000 次请求</p>
</li>
</ul>
<p>内置工具：</p>
<ul>
<li>
<p><code>/memory</code> – 管理对话记忆</p>
</li>
<li>
<p><code>/stats</code> – 查看使用统计</p>
</li>
<li>
<p><code>/tools</code> – 列出可用工具</p>
</li>
<li>
<p><code>/mcp</code> – 配置模型上下文协议服务器</p>
</li>
</ul>
<h4 id="geminicli">Gemini CLI 的限制</h4>
<p>如果您计划使用 Gemini，请考虑以下重要事项：</p>
<ul>
<li>
<p><strong>速率限制</strong> – 免费套餐 60 次请求/分钟，1,000次/天</p>
</li>
<li>
<p><strong>Google 依赖性</strong> – 需要 Google 帐户和互联网连接</p>
</li>
<li>
<p><strong>较新的工具</strong> – 相较于 GitHub Copilot，社区较小，资源较少</p>
</li>
<li>
<p><strong>面向终端</strong> – 与流行的 IDE 整合较少</p>
</li>
<li>
<p><strong>多模态处理</strong> – 图像上传有大小限制 (20MB)</p>
</li>
<li>
<p><strong>测试版功能</strong> – 某些高级功能可能不稳定</p>
</li>
</ul>
<h4 id="gemini">独特的 Gemini 特性</h4>
<p><strong>庞大的上下文窗口：</strong><br>
Gemini 可以在单个会话中处理超过 100 万个标记，这意味着它可以同时分析整个大型代码库。这对于理解复杂系统架构和多个文件之间的关系特别有用。</p>
<p><strong>多模态能力：</strong><br>
Gemini 能够处理和理解各种类型的视觉内容以及代码，使其在设计到代码的工作流程和视觉调试中具有独特的优势。</p>
<h4 id="">将您的草图转化为代码</h4>
<p>这真的很酷：您可以在纸上绘制一些东西，然后 Gemini 将其转化为可工作的代码！</p>
<p>操作方法如下：</p>
<ol>
<li>
<p><strong>创建您的草图：</strong> 在纸上、白板或数位板上绘制您的想法</p>
</li>
<li>
<p><strong>拍照或截图：</strong> 使用手机拍摄或截图，将草图数字化</p>
</li>
<li>
<p><strong>保存图像：</strong> 保存为 JPG、PNG 或 WebP 格式（小于 20MB）</p>
</li>
<li>
<p><strong>通过命令行将其展示给 Gemini：</strong></p>
</li>
</ol>
<pre><code>gemini -p "将此草图转化为具有良好样式的 React 组件" sketch.jpg
</code></pre>
<p><strong>替代方法：</strong></p>
<pre><code># 如果您处于交互会话中，您可以引用文件：
gemini
&gt; 分析此UI草图并创建HTML/CSS：@sketch.jpg

# 或在支持的终端中拖放
gemini
&gt; 将此设计实现为 Vue 组件
[将 sketch.jpg 拖入终端]
</code></pre>
<p>然后，Gemini 会查看您的图纸并生成：</p>
<ul>
<li>
<p>与您的草图匹配的工作 React 组件</p>
</li>
<li>
<p>使其看起来不错的精美 CSS 样式</p>
</li>
<li>
<p>如果您绘制了表单则提供表单验证</p>
</li>
<li>
<p>使其工作的所有代码</p>
</li>
</ul>
<p>这就像拥有一个能读懂您心思的设计师和开发人员！</p>
<h4 id="gemini">通过展示图像来修复 Gemini 中的错误</h4>
<p>UI 出现了 Bug？您可以向 Gemini 展示视觉信息以帮助调试：</p>
<pre><code>gemini -p "该 UI 看起来有问题。有什么问题，我该如何解决？" image.png
</code></pre>
<p>Gemini 可以分析视觉信息并告诉您：</p>
<ul>
<li>
<p>问题的原因是什么</p>
</li>
<li>
<p>需要修改哪些代码</p>
</li>
<li>
<p>有时还有更好的解决方法</p>
</li>
</ul>
<h4 id="">将架构图转化为代码</h4>
<p>画出系统架构图，Gemini 可以构建它：</p>
<pre><code>gemini -p "使用 Docker 和数据库构建此系统架构" diagram.jpg
</code></pre>
<p>Gemini 将：</p>
<ul>
<li>
<p>理解您的图示</p>
</li>
<li>
<p>创建您所需的所有 Docker 文件</p>
</li>
<li>
<p>设置数据库和连接</p>
</li>
<li>
<p>根据您的设计提供一个可工作的系统</p>
</li>
</ul>
<p>将设计翻译成代码不再需要耗费数小时，您可以：</p>
<ol>
<li>
<p>向 Gemini 展示您的草图或设计</p>
</li>
<li>
<p>让 Gemini 构建它</p>
</li>
<li>
<p>在几分钟内获得工作代码，而不是几个小时，只需根据需要进行细化</p>
</li>
</ol>
<p>大多数情况下，Gemini 初次尝试就能很接近您的期望。即使它并不完美，它也为您提供了一个很好的起点，节省了大量时间。</p>
<h3 id="13cli">第13步：比较 CLI 工具</h3>
<p>这里有一个简易表格来帮助您比较 Claude Code 和 Gemini CLI 的功能：</p>
<table>
<thead>
<tr>
<th><strong>功能</strong></th>
<th><strong>Claude Code</strong></th>
<th><strong>Gemini CLI</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>上下文窗口</strong></td>
<td>大</td>
<td>1M+ 令牌</td>
</tr>
<tr>
<td><strong>网络搜索</strong></td>
<td>内置</td>
<td>谷歌搜索集成</td>
</tr>
<tr>
<td><strong>文件编辑</strong></td>
<td>直接编辑</td>
<td>基于差异</td>
</tr>
<tr>
<td><strong>思维模式</strong></td>
<td>4 个等级</td>
<td>ReAct 循环</td>
</tr>
<tr>
<td><strong>IDE 集成</strong></td>
<td>VS Code 快捷键</td>
<td>终端优先</td>
</tr>
<tr>
<td><strong>免费层</strong></td>
<td>有限</td>
<td>慷慨 (1000/天)</td>
</tr>
<tr>
<td><strong>开源</strong></td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td><strong>多模态</strong></td>
<td>否</td>
<td>是 (图像, PDF)</td>
</tr>
</tbody>
</table>
<h3 id="14cli">第14步：高级 CLI 工作流程</h3>
<h4 id="1claudecode">工作流程1：Claude Code 的互动代码审查</h4>
<pre><code>Claude Code &gt; review my recent git changes

[Claude 分析差异]

Claude Code &gt; fix the security issue you found in the login function

Claude Code &gt; now create a pull request with a good description
</code></pre>
<h4 id="2gemini">工作流程2：Gemini 的对话式架构分析</h4>
<pre><code>Gemini &gt; analyze this codebase architecture and identify technical debt

[Gemini 提供全面的分析]

Gemini &gt; create a migration plan for the database issues you found

Gemini &gt; generate API documentation for the endpoints
</code></pre>
<h4 id="3">工作流程3：互动测试驱动开发</h4>
<pre><code>Claude Code &gt; I need to add payment processing. Start by writing comprehensive tests

[Claude 创建测试套件]

Claude Code &gt; now implement the payment service to pass these tests

Claude Code &gt; add error handling and edge cases
</code></pre>
<h3 id="vscodecli">结合 VS Code 和 CLI 工具</h3>
<h4 id="">混合工作流程的力量：</h4>
<p>最具生产力的开发者通常不会只选择一个 AI 工具——他们策略性地将 VS Code 扩展与 CLI 工具结合起来，以最大化效率。每个工具都有独特的优势，结合它们会创造出一个大于其各部分总和的工作流程。</p>
<p><strong>结合工具的好处：</strong></p>
<ul>
<li>
<p><strong>无缝的上下文切换：</strong> 从 Copilot 开始快速开发，然后无缝移动到 Claude Code 进行复杂的分析，而不失去动力</p>
</li>
<li>
<p><strong>互补的优势：</strong> 利用每个工具的最佳功能，如 Copilot 的实时建议 + Claude 的深入推理 + Gemini 的视觉处理</p>
</li>
<li>
<p><strong>持续的工作流：</strong> 无需在工具之间复制/粘贴代码—在项目中直接工作，并根据需要获得不同的 AI 支持</p>
</li>
<li>
<p><strong>减少心理负担：</strong> 工具处理不同的认知任务，让您专注于创造性的问题解决</p>
</li>
</ul>
<h4 id="">如何实际组合工具：</h4>
<p>示例工作流程——构建用户仪表板：</p>
<ol>
<li>
<p><strong>在 VS Code 中使用 Copilot 开始：</strong> 使用制表符补全快速构建基本组件结构</p>
</li>
<li>
<p><strong>保持 VS Code 打开，启动 Claude Code：</strong> 获取架构建议和重构建议，同时保持编辑器上下文</p>
</li>
<li>
<p><strong>切换到 Gemini 以获取视觉元素：</strong> 上传 UI 模型以生成匹配的样式</p>
</li>
<li>
<p><strong>返回到 VS Code：</strong> 应用所有建议，同时有 Copilot 协助实现细节</p>
</li>
</ol>
<p><strong>关键集成点：</strong></p>
<ul>
<li>
<p><strong>共享项目上下文：</strong> 所有工具在同一个目录中工作，了解您的项目结构</p>
</li>
<li>
<p><strong>文件系统协调：</strong> CLI 工具进行的更改会立即在 VS Code 中可见</p>
</li>
<li>
<p><strong>版本控制集成：</strong> 使用 CLI 工具进行 git 操作，而 VS Code 显示视觉差异</p>
</li>
</ul>
<h3 id="">快速切换设置</h3>
<h4 id="">什么是快速切换？</h4>
<p>快速切换设置指的是配置你的开发环境，让你可以快速在不同的 AI 工具之间切换，而不产生摩擦。通过创建快捷方式，而不是输入长命令或通过多个设置步骤进行导航，您可以立即访问当前任务所需的 AI 工具。</p>
<p>在您的 shell 配置文件中添加（<code>.zshrc</code> 或 <code>.bashrc</code>）：</p>
<pre><code># 快速 AI 命令用于交互模式
alias cc="claude"
alias gc="gemini"

# 在需要时进行快速单次命令
alias think="claude 'think hard:'"
alias analyze="gemini -p 'analyze:'"
</code></pre>
<h3 id="3">阶段3实践练习</h3>
<h4 id="1claudecode">练习1：互动 Claude Code 项目设置</h4>
<ol>
<li>
<p>创建一个新项目目录</p>
</li>
<li>
<p>启动：<code>claude</code></p>
</li>
<li>
<p>开始对话："set up a Node.js Express API with PostgreSQL"</p>
</li>
<li>
<p>继续聊天："add authentication middleware"</p>
</li>
<li>
<p>继续："now add comprehensive error handling"</p>
</li>
<li>
<p>查看生成的代码并提问</p>
</li>
</ol>
<h4 id="2gemini">练习2：互动 Gemini 代码分析</h4>
<ol>
<li>
<p>导航到现有项目</p>
</li>
<li>
<p>启动：<code>gemini</code></p>
</li>
<li>
<p>从以下内容开始："analyze this codebase and identify potential security vulnerabilities"</p>
</li>
<li>
<p>后续："explain the most critical issue in detail"</p>
</li>
<li>
<p>继续："create a fix for the authentication vulnerability"</p>
</li>
<li>
<p>询问："what other improvements should I prioritize?"</p>
</li>
<li>
<p>在 VS Code 中使用 Copilot 开始初始开发</p>
</li>
<li>
<p>切换到交互式 Claude Code 会话进行复杂的代码重构</p>
</li>
<li>
<p>使用交互式 Gemini 会话进行代码库分析和文档撰写</p>
</li>
<li>
<p>熟练地在工具之间无缝切换</p>
</li>
</ol>
<p>需要 CLI 工具的帮助吗？请参阅<a href="http://localhost:3333/#troubleshooting-quick-reference">故障排除快速参考</a>以了解设置和常见问题。</p>
<h2 id="stage-4">阶段 4：掌握 - 工具与高级工作流程的结合</h2>
<h3 id="15">第 15 步：工具选择策略</h3>
<h4 id="">何时使用每种工具</h4>
<p>那么，在你的工作流程中，什么时候应该使用每个工具呢？</p>
<p>当速度至关重要时，你可以将 GitHub Copilot 作为一名在线的结对程序员使用。它可以帮助你快速编写新函数、在输入时获得实时建议，并即时掌握不熟悉的 API 或框架。它也方便于在不中断你的工作流程的情况下快速查找文档。</p>
<p>然后，你可以转而使用 Claude Code 来处理更大、更复杂的任务：复杂的多文件重构、起草全面的测试以及“大声思考”关于架构和权衡的问题。在这里，它还可以帮助完成 Git 任务，比如指导你进行操作和组装拉取请求。</p>
<p>最后，当需要端到端分析大型代码库或将视觉输入（如截图/图表）整合到工作流程中时，可以从终端调用 Gemini CLI。由于可以免费使用，所以适合大量运行的场景，并且它适用于需要可定制、脚本友好的设置的场合。</p>
<h3 id="16mcpai">第 16 步：理解 MCP —— 让 AI 工具协同工作</h3>
<h4 id="mcp">什么是 MCP？</h4>
<p>MCP（模型上下文协议）是一种让你的 AI 工具增强能力的简单方法。可以把它想象成给你的手机增添应用程序——每个 MCP 服务器都会为你的 AI 增加新的功能。</p>
<h4 id="mcp">为什么初学者需要关注 MCP？</h4>
<p>没有 MCP 会遇到的问题是：你的 AI 只能处理它所知道的和你告诉它的内容。它无法：</p>
<ul>
<li>
<p>搜索网络上的当前信息</p>
</li>
<li>
<p>自动测试你的网站</p>
</li>
<li>
<p>在会话之间记住你的项目细节</p>
</li>
<li>
<p>连接到你的数据库或 API</p>
</li>
</ul>
<p>但有了 MCP 服务器，你的 AI 就能突然：</p>
<ul>
<li>
<p><strong>获取当前信息</strong> —— 从 Google 搜索最新的文档和解决方案</p>
</li>
<li>
<p><strong>测试你的代码</strong> —— 自动检查你的网站是否正常工作</p>
</li>
<li>
<p><strong>记住你的项目</strong> —— 跟踪你的架构和决策</p>
</li>
<li>
<p><strong>连接到工具</strong> —— 使用 GitHub、数据库等</p>
</li>
</ul>
<p>所以，相比于手动重复劳动，你的 AI 可以自动处理这些任务。这意味着你将花更少的时间去谷歌搜索错误消息、手动测试代码，以及在每个会话中向 AI 解释你的项目，而是花更多时间实际构建东西。</p>
<h4 id="mcp">初学者简单的 MCP 示例</h4>
<p>以下是 MCP 能为你做的一些初学者友好的示例：</p>
<p><strong>示例 1：无需谷歌搜索的帮助</strong></p>
<pre><code>你：“这个 CSS 不工作。找出原因并修复它”

没有 MCP：你需要谷歌搜索错误，阅读文档，尝试解决方案
有了 MCP：AI 搜索当前的 CSS 文档，找出问题，并自动修复
</code></pre>
<p><strong>示例 2：自动测试你的网站</strong></p>
<pre><code>你：“检查我的联系表单是否正常工作”

没有 MCP：你需要手动填写表单，检查邮件，测试边缘案例
有了 MCP：AI 填写表单，确认邮件已发送，测试不同的输入
</code></pre>
<p><strong>示例 3：AI 记住你的项目</strong></p>
<pre><code>你：“为我的待办事项应用程序添加一项新功能”

没有 MCP：你需要解释数据库结构、API 路线、前端框架
有了 MCP：AI 已记住一切并直接构建功能
</code></pre>
<h4 id="mcp">准备尝试 MCP 吗？</h4>
<p>如果这看起来很复杂，不用担心！你可以从一个简单的 MCP 服务开始，在适应后再添加更多。</p>
<h4 id="mcp">初学者简单的 MCP 设置</h4>
<p>我们将从 VS Code 开始（因为这是最简单的选项）：</p>
<ol>
<li>
<p>打开 VS Code</p>
</li>
<li>
<p>转到扩展（Ctrl+Shift+X）</p>
</li>
<li>
<p>搜索“GitHub Copilot MCP”或类似的 MCP 扩展</p>
</li>
<li>
<p>点击“安装”</p>
</li>
</ol>
<p>这样就完成了！扩展会自动处理一切。</p>
<p>通过这样做，你可以为你的 AI 获取网络搜索功能、基本项目记忆力和简单的自动化功能。</p>
<p>要测试它，试着让你的 AI：“搜索最新的 React 最佳实践并给我一个示例”。 如果它能搜索并返回当前的信息，MCP 就正常工作了！</p>
<h4 id="mcp">想要更多的 MCP 功能吗？</h4>
<p>一旦你适应了基本的 MCP，可以探索下面更高级的设置：</p>
<ul>
<li>
<p>自定义 MCP 服务器安装</p>
</li>
<li>
<p>高级配置选项</p>
</li>
<li>
<p>构建自己的 MCP 集成</p>
</li>
</ul>
<p>目前，上述的 VS Code 扩展方法将为你提供充足的 AI 超能力以便入门！</p>
<p>**这就是 MCP 的精要！**从上面的简单 VS Code 扩展方法开始，你将迅速看到你的 AI 变得多么强大。</p>
<h4 id="">下一步</h4>
<ul>
<li>
<p>尝试基础的 VS Code MCP 扩展</p>
</li>
<li>
<p>使用简单的请求进行测试，比如“搜索 X 并实现它”</p>
</li>
<li>
<p>熟悉后，探索更多的第 4 阶段 MCP 服务器</p>
</li>
</ul>
<p>MCP 将你的 AI 从代码建议工具转变为真正的开发伙伴。最好的部分是？一旦你用一个工具设置好，它就会与所有工具协同工作！</p>
<p>如果 AI 提示无法搜索网络，可以尝试以下几种方法。</p>
<p>首先，检查 MCP 扩展是否确实安装在 VS Code 中。然后尝试重启 VS Code。最后，要确保你的提问方式是 AI 能理解的，比如：“搜索 X 并展示 Y”。</p>
<p>如果 VS Code 扩展无法安装，尝试检查你的网络连接或将 VS Code 更新到最新版本。你也可以尝试使用不同名称搜索“MCP”或“Model Context Protocol”扩展。</p>
<p>如果仍然遇到问题，我们将在下文中介绍高级故障排除方法。或者你也可以询问你的 AI：“帮助我排查 MCP 设置故障”。</p>
<h3 id="mcp">高级 MCP 设置与集成</h3>
<h4 id="mcp">手动 MCP 服务器安装</h4>
<p>对于希望完全控制其 MCP 设置的高级用户：</p>
<p><strong>步骤 1：安装 MCP 服务器</strong></p>
<p>大多数 MCP 服务器可以通过 npm 安装：</p>
<pre><code># 用于网络自动化和测试
npm install -g @modelcontextprotocol/server-puppeteer

# 用于无需 API 密钥的网络搜索
npm install -g @mcp-servers/duckduckgo

# 用于数据库访问
npm install -g @modelcontextprotocol/server-postgres
</code></pre>
<p>某些服务器（如 GitHub）则使用 Docker：</p>
<pre><code>docker pull ghcr.io/github/github-mcp-server
</code></pre>
<p><strong>步骤 2：配置你的工具</strong></p>
<p><strong>理解层次化配置：</strong></p>
<p>每个 AI 工具在多个位置检查 MCP 配置，更具体的设置优先级高于一般设置。这意味着你可以有全局默认值，但可以针对特定项目进行覆盖。这就像 CSS——更具体的规则覆盖一般规则。</p>
<p><strong>Claude Code 具有最灵活的设置：</strong></p>
<p>Claude Code 配置层次结构（按顺序检查）：</p>
<ol>
<li>
<p><strong>项目级别</strong>：<code>.claude/mcp.json</code>（最高优先级）</p>
</li>
<li>
<p><strong>本地设置</strong>：<code>.claude/settings.local.json</code></p>
</li>
<li>
<p><strong>全局配置</strong>：<code>~/.claude/mcp.json</code>（后备）</p>
</li>
</ol>
<p>其他工具：</p>
<ul>
<li>
<p><strong>VS Code</strong>：<code>.vscode/mcp.json</code>（仅项目级别）</p>
</li>
<li>
<p><strong>Cursor</strong>：<code>.cursor/mcp.json</code>（仅项目级别）</p>
</li>
<li>
<p><strong>Windsurf</strong>：使用 VS Code 的配置格式</p>
</li>
</ul>
<p>以下是一个示例配置（适用于任何工具，只需调整文件位置）：</p>
<pre><code>{
  "mcpServers": {
    "puppeteer": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-puppeteer"]
    },
    "duckduckgo": {
      "command": "npx",
      "args": ["@mcp-servers/duckduckgo"]
    },
    "github": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "GITHUB_PERSONAL_ACCESS_TOKEN",
        "ghcr.io/github/github-mcp-server"
      ],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "your_token_here"
      }
    }
  }
}
</code></pre>
<h4 id="mcp">生产环境 MCP 服务器</h4>
<p><strong>1. 颠覆性认知工具：</strong></p>
<p><strong>序列思维服务器：</strong><br>
该服务器通过将复杂问题分解为逻辑步骤来改变 AI 处理复杂问题的方式。当你要求实现一个大型功能时，AI 不会直接跳到代码实施，而是首先创建一个详细的计划，其中包含阶段、依赖关系和决策点。</p>
<p>这对于重构遗留系统或构建新功能来说是无价的，尤其是在操作顺序很重要的情况下。服务器在整个开发会话期间保持这种规划上下文，确保决策的一致性。</p>
<p><strong>记忆库服务器：</strong><br>
消除了每次会话都需要重新解释项目结构的烦恼。该服务器会创建有关你的架构选择、编码标准、团队偏好和项目目标的持久性记忆。当你几天后返回工作时，AI 可以立即知道你的数据库架构、API 模式，甚至为什么做出某些决策。它就像一个随着你的开发工作完美同步的项目文档系统。</p>
<p><strong>知识图谱服务器：</strong><br>
创建代码库关系的动态地图——不仅仅是文件依赖，还有功能、共享工具和架构模式之间的概念连接。当你修改一个组件时，AI 可以立即识别出可能需要更新的所有相关区域。这可以防止由于遗漏相关更改而导致的错误，并有助于重构期间的影响分析。</p>
<p><strong>2. 网页自动化与测试服务器：</strong></p>
<p><strong>Puppeteer Server：</strong><br>
提供无头浏览器的控制，用于综合测试工作流。AI 可以自动浏览你的 Web 应用程序，填写表单、点击按钮，并验证预期行为。</p>
<p>这在回归测试中特别强大——AI 可以重播用户工作流并在部署前捕获更改。此外，它还启用基于截图的测试和性能监控自动化。</p>
<p><strong>Playwright Server：</strong><br>
同时扩展 Chrome、Firefox 和 Safari 浏览器的自动化功能。该服务器对于跨浏览器兼容性测试至关重要，并允许 AI 在开发初期捕获特定浏览器的问题。</p>
<p>与手动测试不同，AI 可以在所有浏览器上并行运行相同的测试场景，生成关于功能和性能差异的比较报告。</p>
<p><strong>3. 开发集成服务器：</strong></p>
<p><strong>GitHub Server：</strong><br>
将终端转变为具有 AI 智能的完整 GitHub 界面。AI 可以自动创建分支、管理拉取请求、分析代码审查评论，甚至根据代码更改生成 PR 描述。它还可以根据内容分析分配标签，并通过理解问题与实际代码更改之间的关系来维护项目板。</p>
<p><strong>PostgreSQL 服务器：</strong><br>
支持直接的数据库分析和优化。人工智能可以检查查询性能、建议索引优化、分析数据模式，甚至生成迁移脚本。此服务器在调试生产问题时特别有价值，因为人工智能需要理解实际的数据分布和查询执行模式，而不仅仅是理论上的数据库设计。</p>
<p><strong>4. 辅助工具：</strong></p>
<p><strong>MCP Compass</strong><br>
帮助您为任何任务找到合适的 MCP 服务器。</p>
<p>这些服务器将您的人工智能从代码建议者转变为能够测试、搜索、记忆和自动化的真正开发合作伙伴！</p>
<h3 id="17">步骤 17：高级提示工程</h3>
<h4 id="">上下文提示</h4>
<p>提供示例：</p>
<pre><code>// 替代用语："创建一个验证函数"
// 使用："创建一个类似这样的验证函数但用于电子邮件：
// function validatePhone(phone) { return /^\d{10}$/.test(phone); }"
</code></pre>
<p>指定约束条件：</p>
<pre><code>claude "将此代码重构为使用函数式编程，不使用循环，使用 map/filter/reduce"
</code></pre>
<p>包括边缘案例：</p>
<pre><code>gemini -p "实现用户认证，处理以下情况：过期令牌、并发登录、速率限制"
</code></pre>
<h3 id="18ai">步骤 18：构建 AI 辅助开发流水线</h3>
<h4 id="">自动化代码审查流水线</h4>
<ol>
<li>使用 Copilot 进行预提交检查：</li>
</ol>
<pre><code>// .copilot-instructions
"审核所有更改以发现：安全问题、性能问题、代码风格"
</code></pre>
<ol start="2">
<li>使用 Claude 进行 PR 审查：</li>
</ol>
<pre><code>claude "审查此 PR：git diff main..feature-branch"
</code></pre>
<ol start="3">
<li>使用 Gemini 完成文档：</li>
</ol>
<pre><code>gemini -p "为这些更改生成更新日志并更新 README"
</code></pre>
<h4 id="ai">测试驱动的 AI 开发</h4>
<ol>
<li>编写测试规范：</li>
</ol>
<pre><code>claude "为支付处理系统编写全面的测试规范"
</code></pre>
<ol start="2">
<li>生成测试代码：</li>
</ol>
<pre><code>gemini -p "使用 Jest 实现这些测试规范"
</code></pre>
<ol start="3">
<li>
<p>使用 Copilot 实现：</p>
<ul>
<li>
<p>使用代理模式实现功能</p>
</li>
<li>
<p>测试指导实现过程</p>
</li>
</ul>
</li>
</ol>
<h3 id="19ai">步骤 19：创建您的个人 AI 工作流</h3>
<h4 id="">设置您的环境</h4>
<p>1. VS Code 设置（<code>settings.json</code>）：</p>
<pre><code>{
  "github.copilot.enable": {
    "*": true
  },
  "github.copilot.advanced": {
    "inlineCompletions.enable": true,
    "chat.enabled": true
  }
}
</code></pre>
<p>2. Claude 代码配置（<code>~/.claude/settings.json</code>）：</p>
<pre><code>{
  "cleanupPeriodDays": 7,
  "permissions": {
    "allow": [
      "Bash(fd:*)",
      "Bash(rg:*)",
      "Bash(ls:*)",
      "WebFetch(domain:github.com)",
      "WebFetch(domain:stackoverflow.com)"
    ],
    "deny": ["WebFetch(domain:medium.com)"]
  }
}
</code></pre>
<p>3. Gemini 设置（<code>~/.gemini/config.json</code>）：</p>
<pre><code>{
  "defaultModel": "gemini-2.5-pro",
  "contextWindow": "large",
  "safetyMode": "interactive"
}
</code></pre>
<h4 id="">自定义命令和别名</h4>
<p>为常见任务设置 Shell 别名：</p>
<pre><code># 启动互动会话
alias cc='claude'
alias gc='gemini'

# 快速一次性命令（在需要时）
alias aicommit='claude "创建一个带有描述性信息的 git 提交"'
alias aireview='claude "审查我的未提交更改"'
alias complexity='gemini -p "分析代码复杂性并建议简化"'
alias security='claude "更深入思考：检查安全漏洞"'
alias aidocs='gemini -p "生成全面的文档"'
</code></pre>
<h3 id="ai">最终项目：构建一个完整的应用程序与 AI</h3>
<h4 id="">项目要求</h4>
<p>构建一个任务管理 API，具备：</p>
<ul>
<li>
<p>用户认证</p>
</li>
<li>
<p>CRUD 操作</p>
</li>
<li>
<p>实时更新</p>
</li>
<li>
<p>测试套件</p>
</li>
<li>
<p>文档</p>
</li>
</ul>
<h4 id="">建议工作流程</h4>
<p>阶段 1：互动规划</p>
<pre><code># 启动 Claude 代码会话
claude

Claude Code &gt; 超级思考：设计一个可扩展的任务管理 API 架构

[Claude 提供详细分析]

Claude Code &gt; 现在将其分解为实施阶段

# 切换到 Gemini 进行规格说明
gemini

Gemini &gt; 为此任务管理 API 创建详细的技术规格

Gemini &gt; 包括数据库架构和 API 端点规格
</code></pre>
<p>阶段 2：互动实现</p>
<ol>
<li>
<p>使用 Copilot 代理模式进行初始设置</p>
</li>
<li>
<p>使用内联 Copilot 实现功能</p>
</li>
<li>
<p>切换到互动 Claude 代码会话处理复杂逻辑：</p>
</li>
</ol>
<pre><code>Claude Code &gt; 实施我们计划的用户认证系统

Claude Code &gt; 现在添加任务 CRUD 操作

Claude Code &gt; 用 WebSockets 集成实时更新
</code></pre>
<p>阶段 3：互动测试与文档</p>
<pre><code># Claude 代码会话用于测试
claude

Claude Code &gt; 为所有 API 端点编写全面测试

Claude Code &gt; 为身份验证流程添加集成测试

Claude Code &gt; 为高负载场景创建性能测试

# Gemini 生成文档
gemini

Gemini &gt; 生成包含示例的全面 API 文档

Gemini &gt; 创建开发者入职指南
</code></pre>
<p>阶段 4：互动优化</p>
<pre><code># Claude 代码进行性能优化
claude

Claude Code &gt; 分析并优化我们的数据库查询

Claude Code &gt; 为经常访问的数据实现缓存

Claude Code &gt; 添加监控和日志

# Gemini 进行最终审查
gemini

Gemini &gt; 审查整个代码库以进行改进
</code></pre>
<h3 id="">衡量你的进展</h3>
<h4 id="1">阶段 1 里程碑</h4>
<ul>
<li>
<p>熟悉标签补全</p>
</li>
<li>
<p>可以编写有效的提示语</p>
</li>
<li>
<p>理解 AI 的局限性</p>
</li>
</ul>
<h4 id="2">阶段 2 里程碑</h4>
<ul>
<li>
<p>有效使用多个模型</p>
</li>
<li>
<p>掌握聊天模式和代理</p>
</li>
<li>
<p>使用高级聊天功能</p>
</li>
</ul>
<h4 id="3">阶段 3 里程碑</h4>
<ul>
<li>
<p>熟练使用 CLI 工具</p>
</li>
<li>
<p>可以将 VS Code 和终端工作流结合起来</p>
</li>
<li>
<p>了解工具的优势</p>
</li>
</ul>
<h4 id="4">阶段 4 里程碑</h4>
<ul>
<li>
<p>创建自定义 AI 工作流</p>
</li>
<li>
<p>使用 AI 构建完整应用程序</p>
</li>
<li>
<p>可以教别人 AI 辅助开发</p>
</li>
</ul>
<h3 id="4">阶段 4 练习</h3>
<h4 id="1">练习 1：掌握工具选择</h4>
<ol>
<li>
<p>选择一个中等复杂的编码任务（例如，“构建一个 URL 缩短服务 API”）</p>
</li>
<li>
<p>计划每个阶段使用的工具（设计、编码、测试、部署）</p>
</li>
<li>
<p>按照你选择的工作流程执行</p>
</li>
<li>
<p>记录哪些工作顺利，哪些需要改进</p>
</li>
</ol>
<h4 id="2">练习 2：创建自定义工作流</h4>
<ol>
<li>
<p>识别工作中的重复性开发任务</p>
</li>
<li>
<p>设计一个使用多个工具的 AI 辅助工作流</p>
</li>
<li>
<p>测试并改进该工作流</p>
</li>
<li>
<p>为团队同事创建相关文档</p>
</li>
</ol>
<h4 id="3">练习 3：完整项目构建</h4>
<ol>
<li>
<p>使用 AI 辅助构建一个小型但完整的应用程序</p>
</li>
<li>
<p>战略性地使用至少两种不同 AI 工具</p>
</li>
<li>
<p>包括测试、文档和部署</p>
</li>
<li>
<p>反思比起传统开发的效率提升</p>
</li>
</ol>
<h3 id="">继续你的旅程</h3>
<h4 id="">持续更新</h4>
<ul>
<li>
<p>关注工具的更新说明</p>
</li>
<li>
<p>加入 AI 编码社区</p>
</li>
<li>
<p>试验新功能</p>
</li>
</ul>
<h4 id="">探索高级主题</h4>
<ul>
<li>
<p>自定义 MCP 服务器开发</p>
</li>
<li>
<p>AI 模型微调</p>
</li>
<li>
<p>企业部署策略</p>
</li>
<li>
<p>团队协作模式</p>
</li>
</ul>
<h4 id="">持续学习资源</h4>
<ul>
<li>
<p>每个工具的官方文档</p>
</li>
<li>
<p>社区论坛和 Discord 服务器</p>
</li>
<li>
<p>开源 AI 编码项目</p>
</li>
<li>
<p>会议演讲和教程</p>
</li>
</ul>
<h2 id="common-ai-issues">常见 AI 问题</h2>
<p>即便使用最好的 AI 工具，你仍会遇到挑战。一旦你了解模式，这些问题是正常且可管理的。以下是开发者面临的最常见问题以及实际可行的解决方案。</p>
<h3 id="ai">“我的 AI 建议很糟！”</h3>
<p><strong>问题：</strong> AI 给出的建议无关或错误</p>
<p><strong>解决方案：</strong></p>
<ul>
<li>
<p>写更清晰的注释</p>
</li>
<li>
<p>打开相关文件以提供上下文</p>
</li>
<li>
<p>从简单任务开始</p>
</li>
<li>
<p>确保你在正确的文件类型中</p>
</li>
</ul>
<p><strong>示例修正：</strong></p>
<pre><code>// 原先: "make function"
// 尝试: "create function to validate US phone number format (xxx) xxx-xxxx"
</code></pre>
<h3 id="ai">“AI 太慢”</h3>
<p><strong>问题：</strong> 等待建议时间过长</p>
<p><strong>解决方案：</strong></p>
<ul>
<li>
<p>检查你的网络连接</p>
</li>
<li>
<p>关闭不必要的程序</p>
</li>
<li>
<p>尝试较轻量级的 AI 工具</p>
</li>
<li>
<p>要有耐心 —— 复杂建议需要时间</p>
</li>
</ul>
<h3 id="ai">“我害怕过于依赖 AI”</h3>
<p><strong>问题：</strong> 担心丧失编码能力</p>
<p><strong>解决方案：</strong></p>
<ul>
<li>
<p>将 AI 作为学习工具，而非依赖的工具</p>
</li>
<li>
<p>在接受之前务必理解代码</p>
</li>
<li>
<p>定期练习不使用 AI 编码</p>
</li>
<li>
<p>专注于解决问题，而非语法</p>
</li>
</ul>
<h3 id="">“它建议使用过时代码”</h3>
<p><strong>问题：</strong> AI 建议的模式老旧或使用了废弃的方法</p>
<p><strong>解决方案：</strong></p>
<ul>
<li>
<p>在注释中指定版本</p>
</li>
<li>
<p>保持工具的更新</p>
</li>
<li>
<p>学会识别过时的模式</p>
</li>
</ul>
<p><strong>示例：</strong></p>
<pre><code>// create React functional component using hooks (not class component)
</code></pre>
<h3 id="">故障排除快速参考</h3>
<h4 id="">常见问题（所有工具）</h4>
<table>
<thead>
<tr>
<th><strong>问题</strong></th>
<th><strong>快捷修复</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>没有 AI 建议</td>
<td>检查网络连接，重启编辑器，验证登录</td>
</tr>
<tr>
<td>“需要付款”消息</td>
<td>检查免费层限制，验证账户状态</td>
</tr>
<tr>
<td>建议效果差</td>
<td>使用更清晰的注释，打开相关文件提供上下文</td>
</tr>
<tr>
<td>工具无法安装</td>
<td>更新编辑器，检查网络，尝试不同安装方法</td>
</tr>
</tbody>
</table>
<h4 id="githubcopilot">GitHub Copilot 问题</h4>
<table>
<thead>
<tr>
<th><strong>问题</strong></th>
<th><strong>解决方案</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>VS Code 无建议</td>
<td>检查右下角“GitHub Copilot”状态</td>
</tr>
<tr>
<td>免费层过期</td>
<td>查看 <a href="https://docs.github.com/en/copilot/how-tos/manage-your-account/getting-free-access-to-copilot-pro-as-a-student-teacher-or-maintainer">学生/维护者免费访问</a></td>
</tr>
<tr>
<td>代理模式不起作用</td>
<td>尝试 <code>Shift+Cmd+I</code>（Mac）或 <code>Ctrl+Shift+I</code>（Windows/Linux）</td>
</tr>
<tr>
<td>聊天无响应</td>
<td>尝试重启 VS Code，检查网络连接</td>
</tr>
</tbody>
</table>
<h4 id="claudecode">Claude Code 问题</h4>
<table>
<thead>
<tr>
<th><strong>问题</strong></th>
<th><strong>解决方案</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>“命令未找到”</td>
<td>重新安装：<code>npm uninstall -g @anthropic-ai/claude-code &amp;&amp; npm install -g @anthropic-ai/claude-code</code></td>
</tr>
<tr>
<td>认证失败</td>
<td>运行 <code>claude auth login</code>，检查剩余 API 信用额度</td>
</tr>
<tr>
<td>响应缓慢</td>
<td>网络检查：<code>ping api.anthropic.com</code>，尝试轻量模型：<code>--model claude-3-haiku</code></td>
</tr>
<tr>
<td>MCP 服务器不起作用</td>
<td>检查 <code>~/.claude/mcp.json</code> 语法，测试服务器：<code>npx @mcp/server-github --help</code></td>
</tr>
<tr>
<td>命令挂起/卡住</td>
<td>按 <code>Ctrl+C</code> 取消，重启终端，检查后台进程</td>
</tr>
</tbody>
</table>
<h4 id="geminicli">Gemini CLI 问题</h4>
<table>
<thead>
<tr>
<th><strong>问题</strong></th>
<th><strong>解决方案</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>需要认证</td>
<td>运行 <code>gemini auth login</code>，检查 Google 账户权限</td>
</tr>
<tr>
<td>超过速率限制</td>
<td>检查使用情况：<code>gemini /stats</code>，等待 1 分钟或升级计划</td>
</tr>
<tr>
<td>无法安装</td>
<td>尝试 <code>npx @google/gemini-cli</code>，检查 Node.js 16+</td>
</tr>
<tr>
<td>图片上传失败</td>
<td>检查格式（JPG/PNG/WebP），大小小于 20MB，验证文件路径</td>
</tr>
<tr>
<td>上下文窗口错误</td>
<td>将大请求分割成小部分，清除历史记录</td>
</tr>
</tbody>
</table>
<p>如果一切无效，请按顺序尝试以下方法：</p>
<ol>
<li>
<p>重启你的编辑器/终端</p>
</li>
<li>
<p>检查互联网连接</p>
</li>
<li>
<p>确认你已登录到正确的账户</p>
</li>
<li>
<p>更新工具至最新版本</p>
</li>
<li>
<p>尝试不同的工具（如果一个失败了，通常其他的会有效）</p>
</li>
<li>
<p>询问 AI 自己：“帮助我排查 troubleshoottool_tool_setup”</p>
</li>
</ol>
<h2 id="whats-next-after-completing-all-stages">完成所有阶段后有哪些后续步骤？</h2>
<p>一旦你掌握了基础知识，这里有一些简单的后续步骤：</p>
<h3 id="">与团队合作</h3>
<h4 id="ai">团队 AI 工作流基础</h4>
<p><strong>共享提示库：</strong></p>
<p>构建团队提示库可以改变你整个团队使用 AI 的方式。首先创建一个共享的仓库，让开发者记录适用于你特定领域和代码库的有效提示。</p>
<p>例如，如果你正在构建电子商务软件，为常见任务创建标准化提示，如“生成符合我们 REST 约定的产品目录 API 端点”或“使用我们的标准模式创建支付处理错误处理”。</p>
<p>记录成功的代理模式工作流供团队成员重复使用。某位开发者可能会发现 Claude Code 在给定有关模式演化实践的特定上下文时，特别适合数据库迁移。通过共享这些工作流，你可以避免每个团队成员独立地发现有效的方法。</p>
<p><strong>工具标准化：</strong></p>
<p>当每个人都使用兼容的 AI 工具时，团队生产力会倍增。根据团队的需要确定主要工具——例如，所有开发者都使用 GitHub Copilot，以确保一致的内联支持，再加上 Claude Code 以处理需要深入推理的复杂架构任务。制定清晰的指南，规定何时使用自主的代理模式与协作会话，以防止冲突并确保代码质量。</p>
<p>配置共享的 MCP 服务器设置，以便所有团队成员都能访问相同的增强型 AI 功能。这可能包括内部 API 的团队专用服务器、共享数据库访问或了解你的部署管道的定制工具。当每个人都拥有相同的 AI 功能时，协作就会变得顺畅无比。</p>
<p><strong>AI 生成的代码审查：</strong></p>
<p>将你的代码审查过程转变为与 AI 生成的代码有效协作。为在拉取请求中标记 AI 生成的部分建立惯例——这有助于审查者将注意力集中在适当的地方。审查者可以专注于架构决策、业务逻辑正确性和需要人工判断的集成模式，而不是挑剔 AI 通常处理得很好的语法。</p>
<p>对 AI 生成的代码实施严格的测试，因为自动化测试比手动审查更可靠地捕捉 AI 错误。为测试 AI 输出创建团队标准，包括 AI 可能遗漏的边缘案例和集成场景。这样你就能在享受 AI 带来的速度的同时，通过系统的验证保持质量。</p>
<p><strong>在提交信息中记录 AI 工具决策。</strong></p>
<h4 id="">简单的团队设置</h4>
<p>从小处着手，逐步建立：</p>
<ul>
<li>
<p>首先让每个人使用相同的 AI 工具</p>
</li>
<li>
<p>创建一个适合你项目的提示共享文档</p>
</li>
<li>
<p>找出团队何时应该使用代理模式与常规协助模式</p>
</li>
<li>
<p>为你最重要的团队工具设置 MCP 服务器</p>
</li>
</ul>
<h3 id="">对于更大的项目</h3>
<p>随着项目的发展，你可能会想要：</p>
<ul>
<li>
<p>为不同的任务尝试不同的 AI 模型（简单代码用快速的，复杂问题用强大的）</p>
</li>
<li>
<p>为你经常执行的任务创建快捷方式</p>
</li>
<li>
<p>将 AI 工具与现有开发工作流连接</p>
</li>
</ul>
<h3 id="">持续学习</h3>
<p>AI 编码工具每个月都在进步！通过以下方式保持最新：</p>
<ul>
<li>
<p>关注工具的发行说明（他们会发送更新邮件）</p>
</li>
<li>
<p>加入 AI 编码的 Discord 社区</p>
</li>
<li>
<p>尝试新功能，因为它们不断出现</p>
</li>
</ul>
<h2 id="conclusion">结语</h2>
<p>恭喜你！你现在已经拥有了开启 AI 辅助编码之旅的所有必需品。记住，每个专家都曾是初学者，而有 AI 作为你的编码伙伴，你可以比以往更快地学习和成长。</p>
<p><strong>记住：</strong></p>
<ul>
<li>
<p>AI 不会取代你的创造力——它会放大创造力</p>
</li>
<li>
<p>每个建议都是一次学习的机会</p>
</li>
<li>
<p>错误是旅程的一部分</p>
</li>
<li>
<p>社区在这里帮助你</p>
</li>
</ul>
<p>你不仅仅是在学习用 AI 编码——你是在了解软件开发的未来。几个月后，你会想知道自己过去是如何在没有它的情况下编码的。今天拥抱 AI 辅助的开发者将成为明日的领袖。</p>
<p>Happy coding! 🚀</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 来自 freeCodeCamp 社区的圣诞礼物：学习 Python、SQL、西班牙语等课程 ]]>
                </title>
                <description>
                    <![CDATA[ 2025 年对全球 freeCodeCamp 社区来说是精彩纷呈的一年。我们非常激动地以一系列圣诞礼物为这一年画上圆满句号：  1. freeCodeCamp Python 认证  2. freeCodeCamp JavaScript 认证（第 10 版）  3. freeCodeCamp 响应式 Web 设计认证（第 10 版）  4. freeCodeCamp 关系型数据库 + SQL 认证  5. 我们的 A2 级开发者英语认证  6. 我们的 B1 级开发者英语认证  7. 我们的 A1 级西班牙语课程（测试版）  8. 我们的 A1 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/christmas-gifts-freecodecamp-community-2025/</link>
                <guid isPermaLink="false">694b60661acf9a04cd95df3e</guid>
                
                    <category>
                        <![CDATA[ 社区 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ freeCodeCamp ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Wed, 24 Dec 2025 05:29:57 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2025/12/----_20251224114011_141_113.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/christmas-gifts-freecodecamp-community-2025/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Christmas gifts for you from the freeCodeCamp community: Learn Python, SQL, Spanish, and more</a>
      </p><p>2025 年对全球 freeCodeCamp 社区来说是精彩纷呈的一年。我们非常激动地以一系列圣诞礼物为这一年画上圆满句号：</p><ol><li>freeCodeCamp Python 认证</li><li>freeCodeCamp JavaScript 认证（第 10 版）</li><li>freeCodeCamp 响应式 Web 设计认证（第 10 版）</li><li>freeCodeCamp 关系型数据库 + SQL 认证</li><li>我们的 A2 级开发者英语认证</li><li>我们的 B1 级开发者英语认证</li><li>我们的 A1 级西班牙语课程（测试版）</li><li>我们的 A1 级中文课程（测试版）</li></ol><p>如此丰厚的礼物，我们开始拆封吧！</p><h2 id="-10-">编程认证与全栈开发课程第 10 版</h2><p>在过去的 11 年里，freeCodeCamp 社区对核心编程课程体系进行了多次构建与重构。</p><p>如今我们终于实现编程课程兼具全面性与互动性的愿景。</p><p>第 10 版课程包含 6 项认证，每项认证都包含十余个实践项目，帮助你扎实掌握核心技能。</p><!--kg-card-begin: markdown--><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766529240482/d07520b2-ba51-427d-a053-403381c4d185.webp" alt="我们刚刚发布的 Python 课程的截图" class="image--center mx-auto" width="1654" height="1446" loading="lazy"><!--kg-card-end: markdown--><p>在每项认证结束时，你将参加一次期末考试。如果你能通过考试，将获得一份免费的认证证书。你可以将此证书加入 LinkedIn 个人资料，或添加到你的简历、履历或个人作品集网站中。</p><p>截至目前，已有 4 项认证正式上线：</p><ul><li><a href="https://www.freecodecamp.org/news/freecodecamps-new-responsive-web-design-certification-is-now-live/">响应式 Web 设计认证发布</a></li><li><a href="https://www.freecodecamp.org/news/freecodecamps-new-javascript-certification-is-now-live/">JavaScript 认证发布</a></li><li><a href="https://www.freecodecamp.org/news/freecodecamps-new-python-certification-is-now-live/">Python 认证发布</a></li><li><a href="https://www.freecodecamp.org/news/freecodecamps-new-relational-databases-certification-is-now-live/">关系型数据库认证发布</a></li></ul><p>我们将在 2026 年发布前端库认证与后端开发认证。</p><p>在获得全部 6 项认证后，你可以构建一个最终的毕业项目，该项目将由经验丰富的开发者进行代码审查。随后你将参加综合期末考试，通过后即可获得我们最终的全栈开发工程师认证。</p><p>如果你现在就开始学习前四项认证，那么在你学完之前，后两项认证也会发布了。毕竟，每项认证都涵盖了数百小时的理论计算机科学知识和动手编程实践。</p><h2 id="-">语言课程</h2><p>你可能会问：freeCodeCamp 是从何时开始教授世界语言的？</p><p>实际上，我们从 2022 年就开始设计“开发者英语”课程体系。在过去的几年间，我们对其进行了大幅扩展。</p><p>该课程通过手绘动画角色进行互动教学。在学习过程中，你将进行大量的阅读、写作、听力练习，口语练习功能也将在 2026 年上线。</p><!--kg-card-begin: markdown--><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766527685142/e1cc58c5-a245-4777-b2cf-b97dd7790d27.webp" alt="语言学习的 6 个 CEFR 级别图表" class="image--center mx-auto" width="1024" height="768" loading="lazy"><!--kg-card-end: markdown--><p>这是一套以故事驱动的课程体系。你将化身为一位刚抵达加利福尼亚、即将加入科技初创企业的开发者，在新生活的日常互动中，你将逐步掌握语法、词汇、技术术语乃至地道俚语。</p><!--kg-card-begin: markdown--><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766527748381/abc33dff-e9bf-4efb-bd4c-46362e42f288.webp" alt="开发者英语课程的截图" class="image--center mx-auto" width="856" height="656" loading="lazy"><!--kg-card-end: markdown--><p>截至目前，已有两项语言认证正式上线：</p><ul><li><a href="https://www.freecodecamp.org/news/freecodecamps-a2-english-for-developers-certification-is-now-live/">A2 级别英语认证发布</a></li><li><a href="https://www.freecodecamp.org/news/freecodecamps-b1-english-for-developers-certification-is-now-live/">B1 级别英语认证发布</a></li></ul><p>我们正在开发 A1、B2、C1 和 C2 级别的课程，计划在未来几年陆续发布（是的，以年为单位。每个级别的开发都是庞大的工程）。</p><p>freeCodeCamp 社区不仅设计了数千节英语课程，还开发了大量定制软件工具来实现这些课程体系。因此在 2024 年，我们提出设想：能否用同样的工具来教授西班牙语和中文普通话？</p><p>如今，这项努力的成果已进入公开测试阶段。我们从这两种语言的 A1 级别课程起步，其余级别将在未来几年逐步推出。</p><ul><li><a href="https://www.freecodecamp.org/news/freecodecamps-a1-professional-spanish-curriculum-beta-is-now-live/">A1 级别西班牙语课程发布</a></li><li><a href="https://www.freecodecamp.org/news/freecodecamps-a1-professional-chinese-curriculum-beta-is-now-live/">A1 级别中文课程发布</a></li></ul><h2 id="--1">为何教授西班牙语和中文？</h2><p>除英语外，西班牙语和中文（普通话）是全球使用最广泛的两种语言。掌握这些语言可帮助你参与众多网络社区、探访主要城市，甚至获得新的工作机会。</p><p>学习外语对大脑神经可塑性大有裨益，并且可以与编程等其他技能学习同步开展。</p><p>现在你可以通过我们完整的端到端课程体系免费学习这些语言，所有课程均由教师、译者和母语者共同设计。</p><h2 id="-freecodecamp-">关于将 freeCodeCamp 课程翻译为世界主要语言的进展</h2><p>你可能知道，freeCodeCamp 自 2020 年起已支持多种世界主要语言。但每当我们发布新课程时，全部内容的翻译仍需数月时间。</p><p>值得欣慰的是，机器翻译技术在过去几年持续进步。</p><p>社区仍在人工翻译教程和书籍，但对于 freeCodeCamp 编程课程这类更新迅速的内容，我们需要加速这一进程。</p><p>我们已试点将全部新课程翻译为西班牙语和葡萄牙语。</p><ul><li>首先运用前沿大语言模型，结合大量术语表和风格指南，处理编程课程中数十万词的原始内容，</li><li>随后由母语者随机抽样审校以保证质量。</li><li>当我们确认翻译质量达标后，便开始建立数据管道，通过开源代码贡献实现英文原文更新时的自动翻译同步。</li></ul><p>这些工作的经济成本并不高昂，因此我们应该能在此基础上，进一步将 freeCodeCamp 编程课程拓展到此前未能支持的其他语言，例如阿拉伯语和法语。</p><!--kg-card-begin: markdown--><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766528132885/06da1977-85b8-438e-8e80-1818f3dba7e7.webp" alt="freeCodeCamp 的编程课程被翻译为葡萄牙语的截图" class="image--center mx-auto" width="1017" height="918" loading="lazy"><!--kg-card-end: markdown--><p>如果你是多年来参与过 freeCodeCamp 翻译的数百位贡献者之一，我们依然非常欢迎你帮助翻译书籍和教程，这些内容在首次发布后通常不会有太大改动。</p><p>毕竟，文档本地化的最高标准是由一位译者完整阅读并理解原文后再进行翻译。</p><h2 id="--2">我们的社区才刚刚启航。</h2><p>今年，freeCodeCamp 社区还实现了：</p><ul><li>在 freeCodeCamp YouTube 频道上发布 129 个免费视频课程</li><li>在 freeCodeCamp 专栏上发布 45 本免费完整书籍和手册</li><li>452 篇关于数学、编程和计算机科学的教程和文章</li><li>50 集 freeCodeCamp 播客节目，我采访了许多开发者，其中许多人都是 freeCodeCamp 开源项目的贡献者</li></ul><p>我们还向 freeCodeCamp 开源学习平台合并了 4,279 次代码提交，这些更新大幅提升了用户体验与可访问性。同时我们发布了安全的考试环境，让 campers 能够参加认证考试。</p><p>你可以查看我们的 <strong><a href="https://www.freecodecamp.org/chinese/news/freecodecamp-2025-top-contributors/">2025 年度的顶级开源贡献者榜单</a></strong>。</p><p>作为一个社区，我们才刚刚起步。免费的开源教育在当今时代的意义前所未有地重大。</p><h2 id="--3">我们也邀请你更深入地参与社区。</h2><p>我要感谢每月支持我们的慈善事业与使命的 10,221 位热心人士。请考虑加入这个行列：<a href="https://www.freecodecamp.org/donate/">捐赠支持 freeCodeCamp.org</a>。</p><p>这里有其他几种<a href="https://www.freecodecamp.org/news/how-to-donate-to-free-code-camp/">可抵扣美国税款的年终捐赠方式</a>。</p><p>freeCodeCamp 拥有一个充满活力的全球社区，聚集了众多积极进取的学习者，他们正在掌握新技能并为职业发展的下一阶段做准备。我鼓励你<a href="https://discord.com/servers/freecodecamp-692816967895220344">加入 freeCodeCamp Discord 与我们交流互动</a>。</p><p>同时，请参与 <a href="https://forms.nhcarrigan.com/o/docs/forms/7LNb8jFoN4SPBvP7vRxDi2/4">Naomi 的 freeCodeCamp 社区调查</a>，帮助我们了解你喜爱 freeCodeCamp 的哪些方面，以及我们的社区可以在哪些方面做得更好。</p><p>我谨代表全球 freeCodeCamp 社区，祝愿你和家人在 2025 年收官之际一切圆满。也让我们共同举杯，迎接充满乐趣与雄心壮志的 2026 年。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ freeCodeCamp A1 专业中文课程（测试版）上线 ]]>
                </title>
                <description>
                    <![CDATA[ freeCodeCamp 社区刚刚发布了我们新的 A1 专业中文课程的前几个章节。你现在可以利用这些内容开始学习中文。 每一章都包含数百个互动任务，旨在帮助你自信地迈出学习中文的第一步。 新的 A1 专业中文课程是怎样的？ 在这个 A1 专业中文课程中，你将学习中文的基础知识。这将遵循欧洲共同语言参考标准（CEFR）的 A1 要求。我们专注于在专业环境中特别有用的词汇。 课程包含多个模块，包括热身、学习、练习、复习和测验，以确保你真正理解了一个模块再进入下一个。 热身是准备部分，为模块的主要内容提供背景。 热身中的任务将向你介绍新词汇，或复习你已经学习过并将在当前模块中使用的内容。 下面是在课程中你将会遇到的一个示例。 每个任务都有一个相应的问题，帮助你练习内容。如果你不知道如何回答问题或需要更多细节，你可以查看解释部分。 在热身之后，你将进入学习 部分。在这里，你会看到你新学的单词在实际中的应用！你将聆听短片段的独白或对话，并回答关于它们的问题，以确保你理解它们的含义以及它们在真实对话中的使用方式。在必要时，你还将在这里学习一些理论。 课程中还包含填 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/freecodecamps-a1-professional-chinese-curriculum-beta-is-now-live/</link>
                <guid isPermaLink="false">694a9c8c1acf9a04cd95df2a</guid>
                
                    <category>
                        <![CDATA[ 社区 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ freeCodeCamp ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Tue, 23 Dec 2025 05:55:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2025/12/----_20251224135437_142_113.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/freecodecamps-a1-professional-chinese-curriculum-beta-is-now-live/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">freeCodeCamp's A1 Professional Chinese Curriculum (Beta) is Now Live</a>
      </p><!--kg-card-begin: markdown--><p>freeCodeCamp 社区刚刚发布了我们新的 A1 专业中文课程的前几个章节。你现在可以利用这些内容开始学习中文。</p>
<p>每一章都包含数百个互动任务，旨在帮助你自信地迈出学习中文的第一步。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766158559146/126837fd-0786-476d-b643-1eed59e87814.png" alt="A1 专业中文课程大纲。章节包括欢迎、拼音课程，如简单韵母和鼻音韵母，以及问候和介绍。每个章节的完成状态均有显示。" width="848" height="595" loading="lazy"></p>
<h2 id="a1">新的 A1 专业中文课程是怎样的？</h2>
<p>在这个 A1 专业中文课程中，你将学习中文的基础知识。这将遵循欧洲共同语言参考标准（CEFR）的 A1 要求。我们专注于在专业环境中特别有用的词汇。</p>
<p>课程包含多个模块，包括热身、学习、练习、复习和测验，以确保你真正理解了一个模块再进入下一个。</p>
<p><strong>热身</strong>是准备部分，为模块的主要内容提供背景。</p>
<p>热身中的任务将向你介绍新词汇，或复习你已经学习过并将在当前模块中使用的内容。</p>
<p>下面是在课程中你将会遇到的一个示例。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766158665243/cb5a3d23-3626-4b12-aae0-829a3db7b7cf.png" alt="一个穿条纹毛衣的人在一个现代办公室设置的虚拟教室中。文字写道，“还记得一个音节有三个部分：声母、韵母和声调吗？这些部分的任何变化都可能表示完全不同的字符。”对话框显示“你好”的拼音和汉字。" width="960" height="647" loading="lazy"></p>
<p>每个任务都有一个相应的问题，帮助你练习内容。如果你不知道如何回答问题或需要更多细节，你可以查看解释部分。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764863508777/7e6f988c-9bf7-454c-afaa-091920ff5a02.png" alt="7e6f988c-9bf7-454c-afaa-091920ff5a02" width="940" height="725" loading="lazy"></p>
<p>在<strong>热身</strong>之后，你将进入<strong>学习</strong>部分。在这里，你会看到你新学的单词在实际中的应用！你将聆听短片段的独白或对话，并回答关于它们的问题，以确保你理解它们的含义以及它们在真实对话中的使用方式。在必要时，你还将在这里学习一些理论。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766158808549/5a1b2945-149a-433b-a840-cff49224908c.png" alt="在一个有大窗的城市景观的办公室房间里站立的动画角色。上方文字解释如何用中文介绍国籍，“wǒ shì + 国籍”，下方字幕显示角色用拼音和简体中文字符说“我是中国人”。" width="835" height="654" loading="lazy"></p>
<p>课程中还包含填空题，帮助你练习使用拼音和汉字。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766158976101/208a38c7-850d-4d43-8794-c6c51fc92962.png" alt="一个背着背包的动画角色站在有绿色植物墙和电脑的办公室里。显示中文文本和拼音，内容为：“我是新加坡人。”下方有与句子相关的填空题目。" width="816" height="770" loading="lazy"></p>
<p>在<strong>学习</strong>之后，你将继续进入<strong>练习</strong>部分，在这里你将完成更多开放式任务，以测试你是否理解以及使用拼音和汉字写作的能力。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766159066500/9b4b962d-dcd8-4782-9526-07a4cc5e0219.png" alt="一个语言练习的截图，包含中文文本和拼音，指示用户将“我是、开发者、中国人、你好”按正确顺序放到相应位置。“wǒ shì” 意思是“我是”。这些短语需要放置在“wáng huá”和其他提示旁边的空白处。" width="805" height="444" loading="lazy"></p>
<p>在模块的末尾，有一个<strong>复习</strong>部分，包含语法要点、主要词汇和概念。你可以使用这些复习页面帮助你准备测验。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766097053581/bc30af81-4d64-42b2-84d5-624d9c3f87db.png" alt="这幅图片包含一个题为“语法要点”的文本，讨论中文中的问候类型。它介绍了三个短语：“你好” (nǐ hǎo) 用于随意的场合，“您好” (nín hǎo) 用于礼貌互动，“你们好” (nǐ men hǎo) 用于与多人打招呼。" width="1570" height="1042" loading="lazy"></p>
<p>模块的最后部分是<strong>测验</strong>，旨在检验你对模块中所涵盖材料的理解。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766162326716/7f0a788e-5f9b-4b3b-8499-1cd39eca1d37.png" alt="名为“问候与介绍测验”的页面检测你对基础中文问候的理解。要通过，必须正确回答至少 10 道题中的 9 道。第一个问题询问在向一群人问候时应该使用哪个短语，选项包括“nǐ men hǎo” (你们好)、“nín hǎo” (您好)、“wǒ shì” (我是) 和“nǐ hǎo” (你好)。" width="856" height="695" loading="lazy"></p>
<p>认证考试将作为该认证的最后一项发布。我们目前发布了前三章，后续章节将由我们的教学设计团队开发后逐步发布。当所有章节上线后，我们将发布认证考试。</p>
<h2 id="">致谢贡献者</h2>
<p>感谢以下贡献者在课程开发中提供帮助：</p>
<ul>
<li>
<p><a href="https://github.com/gikf">Krzysztof G.</a></p>
</li>
<li>
<p><a href="https://github.com/mrchenguozheng">Chen Guo Zheng</a></p>
</li>
<li>
<p><a href="https://github.com/stevending1st">Steven Ding</a></p>
</li>
</ul>
<p>我们特别感谢 <a href="https://github.com/s1ngs1ng">S1ng S1ng</a>，他录制了独白和对话音频，还录制了生动展示拼音发音的教学视频。这些拼音视频将在明年陆续添加到课程中。</p>
<h2 id="">常见问题</h2>
<h3 id="">这些内容真的全部免费吗？</h3>
<p>是的。freeCodeCamp 一直以来都是免费的，并且我们已经提供免费的认证超过十年。</p>
<h3 id="">我可以用除英语以外的语言学习中文课程吗？</h3>
<p>我们的目标是让 freeCodeCamp 支持的语言课程都能以各种语言提供。查看你的账户设置，看看你正在学习的课程是否已经提供你偏好的语言版本。</p>
<h3 id="">中文课程涵盖哪些语言技能？</h3>
<p>语言课程目前涵盖听力、阅读和写作。我们计划稍后添加口语部分。</p>
<h3 id="">语言课程和考试中的音频是由母语者录制的吗？</h3>
<p>是的。语言课程中的所有音频均由该语言的母语者录制。</p>
<h3 id="">我是聋人或听力不佳者。我还可以学习语言课程吗？</h3>
<p>可以！所有音频课程都提供字幕和可阅读的文本。</p>
<h3 id="">我是盲人或视力有限，并使用屏幕阅读器。我还可以学习语言课程吗？</h3>
<p>可以！freeCodeCamp 课程设计为无障碍，你可以使用屏幕阅读器学习语言课程。如果遇到任何无障碍问题，你可以在我们的 GitHub 仓库上反馈，以便社区解决。</p>
<h3 id="a1a2b1">课程名称旁边的字母和数字有什么意义？（例如：A1、A2、B1）</h3>
<p>这些标记指的是 CEFR（欧洲共同语言参考标准）等级，这是一种用于描述语言熟练程度的国际框架。A1 和 A2 代表初级水平，B1 和 B2 代表中级水平，C1 和 C2 代表高级水平。每个等级指示你在语言学习阶段所预期具备的技能和知识。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765475620837/6822cf7c-d05f-4a5c-8e8a-994c540ed369.png" alt="一张标题为“CEFR 等级详解”的图表展示了从 A1 到 C2 的六个 CEFR 语言能力等级。A1 等级描述为“在倾听者的帮助下进行交流”。A2 等级涉及“在有限范围内进行交流”。B1 等级重点在于“在熟悉的情境中交流核心要点”。B2 等级能够在“多种情境中较流畅地交流”。C1 等级强调“在多数情境中灵活流畅地沟通”。C2 等级代表“在绝大多数情境中实现精准细腻的交流”。freeCodeCamp 标识位于图表底部。" width="1024" height="768" loading="lazy"></p>
<h2 id="">其他</h2>
<p>祝你在 freeCodeCamp 的语言课程学习中好运。</p>
<p>祝学习愉快！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ freeCodeCamp 开源社区 2025 年度 Top Contributors ]]>
                </title>
                <description>
                    <![CDATA[ 2025 年对于全球 freeCodeCamp 社区来说是非常富有成效的一年。作为一个已经走过 12 个年头的社区，我们在各个方面都全速前进，比以往更加稳步推进。 今年，我们对新的全栈开发者课程进行了重大改进。这是 freeCodeCamp 课程的第 10 个版本，包含 7 个认证。在学习过程中，学习者将完成超过 100 个实践项目，并通过计算机科学理论的考试。 此外，在过去的一年里，freeCodeCamp 社区实现了：  * 在 freeCodeCamp YouTube 频道上发布 129 个免费视频课程          * 在 freeCodeCamp 专栏上发布 45 本免费完整书籍和手册          * ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/freecodecamp-2025-top-contributors/</link>
                <guid isPermaLink="false">6947ed6d1acf9a04cd95ded6</guid>
                
                    <category>
                        <![CDATA[ 社区 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ freeCodeCamp ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Mon, 22 Dec 2025 11:18:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2025/12/----_20251222205523_138_113.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/freecodecamp-top-open-source-contributors-2025/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">freeCodeCamp's Top Open Source Contributors of 2025</a>
      </p><!--kg-card-begin: markdown--><p>2025 年对于全球 freeCodeCamp 社区来说是非常富有成效的一年。作为一个已经走过 12 个年头的社区，我们在各个方面都全速前进，比以往更加稳步推进。</p>
<p>今年，我们对新的全栈开发者课程进行了重大改进。这是 freeCodeCamp 课程的第 10 个版本，包含 7 个认证。在学习过程中，学习者将完成超过 100 个实践项目，并通过计算机科学理论的考试。</p>
<p>此外，在过去的一年里，freeCodeCamp 社区实现了：</p>
<ul>
<li>
<p>在 freeCodeCamp YouTube 频道上发布 129 个免费视频课程</p>
</li>
<li>
<p>在 freeCodeCamp 专栏上发布 45 本免费完整书籍和手册</p>
</li>
<li>
<p>452 篇关于数学、编程和计算机科学的教程和文章</p>
</li>
<li>
<p>50 集 freeCodeCamp 播客节目，我采访了许多开发者，其中许多人都是 freeCodeCamp 开源项目的贡献者</p>
</li>
</ul>
<p>我们还合并了 4,279 个提交到 freeCodeCamp 的开源学习平台，进行了大量用户体验和无障碍访问的改进。此外，我们还发布了安全的考试环境，供学习者参加认证考试。</p>
<p>最后，我们在开发者英语课程上取得了相当大的进展，并开始制作即将上线的西班牙语和中文课程。</p>
<p>我们才刚刚开始。我们已经在规划关于数学、数据科学、机器学习和其他深度、技能密集型课程的附加课程。</p>
<p>这一切都要感谢 10,342 名善良的人们<a href="https://donate.freecodecamp.org">捐助支持我们的慈善事业和使命</a>，以及那些投入时间和才华给社区的人们。</p>
<p>以下是 2025 年我们社区的 611 位最活跃的开源贡献者名单：</p>
<h2 id="githubtopcontributor">GitHub Top Contributor</h2>
<ul>
<li>
<p><a href="https://github.com/clarencepenz">Clarence Bakosi</a></p>
</li>
<li>
<p><a href="https://github.com/Supravisor">Supravisor</a></p>
</li>
<li>
<p><a href="https://github.com/Giftea">Giftea ☕</a></p>
</li>
<li>
<p><a href="https://github.com/pdtrang">Diem-Trang Pham</a></p>
</li>
<li>
<p><a href="https://github.com/a2937">Anna</a></p>
</li>
<li>
<p><a href="https://github.com/c0d1ng-ma5ter">c0d1ng_ma5ter</a></p>
</li>
<li>
<p><a href="https://github.com/JungLee-Dev">JungLee-Dev</a></p>
</li>
<li>
<p><a href="https://github.com/hbar1st">hbar1st</a></p>
</li>
<li>
<p><a href="https://github.com/dev-kamil">dev-kamil</a></p>
</li>
<li>
<p><a href="https://github.com/arizfaiyaz">Ariz Faiyaz</a></p>
</li>
<li>
<p><a href="https://github.com/agilan11">agilan11</a></p>
</li>
<li>
<p><a href="https://github.com/cuongpham24">Vinson Pham</a></p>
</li>
<li>
<p><a href="https://github.com/StuartMosquera">Stuart Mosquera</a></p>
</li>
<li>
<p><a href="https://github.com/StephenMuya">Stephen Mutheu Muya</a></p>
</li>
<li>
<p><a href="https://github.com/MohamadSalman11">Mohamad Salman</a></p>
</li>
<li>
<p><a href="https://github.com/alexgoldsmith">Alex Goldsmith</a></p>
</li>
<li>
<p><a href="https://github.com/vishnudt2004">Vishnu D</a></p>
</li>
<li>
<p><a href="https://github.com/kannan-ravi">Kannan</a></p>
</li>
<li>
<p><a href="https://github.com/tanmaygautam11">Tanmay Gautam</a></p>
</li>
<li>
<p><a href="https://github.com/l3onhard">l3onhard</a></p>
</li>
<li>
<p><a href="https://github.com/prabhakaryadav2003">Prabhakar Yadav</a></p>
</li>
<li>
<p><a href="https://github.com/errantpianist">Ezoh Zhang</a></p>
</li>
<li>
<p><a href="https://github.com/Ajay-2005">Ajay A</a></p>
</li>
<li>
<p><a href="https://github.com/dragon-slayer27">Vivaan Teotia</a></p>
</li>
<li>
<p><a href="https://github.com/hassanwaqa">Hassan Waqar</a></p>
</li>
<li>
<p><a href="https://github.com/gikf">Krzysztof G.</a></p>
</li>
<li>
<p><a href="https://github.com/roberiacono">Roberto Iacono</a></p>
</li>
<li>
<p><a href="https://github.com/MelvinManni">Melvin Kosisochukwu</a></p>
</li>
<li>
<p><a href="https://github.com/lasjorg">Lasse Jørgensen</a></p>
</li>
<li>
<p><a href="https://github.com/dennmar">dennmar</a></p>
</li>
<li>
<p><a href="https://github.com/Arif-Khalid">Arif Khalid</a></p>
</li>
<li>
<p><a href="https://github.com/soryaek">Sorya Ek</a></p>
</li>
<li>
<p><a href="https://github.com/RaymondLiu777">Raymond Liu</a></p>
</li>
<li>
<p><a href="https://github.com/AyushSharma72">Ayush Sharma</a></p>
</li>
<li>
<p><a href="https://github.com/yusufasur">Yusuf Can Aşur</a></p>
</li>
<li>
<p><a href="https://github.com/kb42">Karthik Bagavathy</a></p>
</li>
<li>
<p><a href="https://github.com/Sky-walkerX">Naman Khandelwal</a></p>
</li>
<li>
<p><a href="https://github.com/vkalakota18">Varshith Kalakota</a></p>
</li>
<li>
<p><a href="https://github.com/omarraf">Omar Rafiq</a></p>
</li>
<li>
<p><a href="https://github.com/raahthor">Prashant Rathore</a></p>
</li>
<li>
<p><a href="https://github.com/Agung1606">Agung Saputra</a></p>
</li>
<li>
<p><a href="https://github.com/sanchitkhthpalia">Sanchit Kathpalia</a></p>
</li>
<li>
<p><a href="https://github.com/shashankdangi">Shashank Dangi</a></p>
</li>
<li>
<p><a href="https://github.com/garyeung">Gary Yeung</a></p>
</li>
<li>
<p><a href="https://github.com/ppl-call-me-tima">Amit Upadhyay</a></p>
</li>
<li>
<p><a href="https://github.com/pkdvalis">pkdvalis</a></p>
</li>
<li>
<p><a href="https://github.com/sinha21Soumya">sinha21Soumya</a></p>
</li>
<li>
<p><a href="https://github.com/VishalTelukula">Telukula Vishal</a></p>
</li>
<li>
<p><a href="https://github.com/sskiragu">sskiragu</a></p>
</li>
<li>
<p><a href="https://github.com/AishwaryaRajput09">Aishwarya</a></p>
</li>
<li>
<p><a href="https://github.com/TrevorBrowning">Trevor Browning</a></p>
</li>
<li>
<p><a href="https://github.com/AilaLu">AilaLu</a></p>
</li>
<li>
<p><a href="https://github.com/zxc-w">zxc-w</a></p>
</li>
<li>
<p><a href="https://github.com/anishlukk123">Anish Lukkireddy</a></p>
</li>
<li>
<p><a href="https://github.com/josue-igiraneza">Josue Igiraneza</a></p>
</li>
<li>
<p><a href="https://github.com/adityaravichandran6">Aditya Ravichandran</a></p>
</li>
<li>
<p><a href="https://github.com/skyewm">Skye Mickens</a></p>
</li>
<li>
<p><a href="https://github.com/gagan-bhullar-tech">Gagan Bhullar</a></p>
</li>
<li>
<p><a href="https://github.com/asr1325">Aditya</a></p>
</li>
</ul>
<h2 id="topcontributor">论坛 Top Contributor</h2>
<ul>
<li>
<p><a href="https://forum.freecodecamp.org/u/Teller">Teller</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/pkdvalis">pkdvalis</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/hasanzaib1389">Hassan Zaib</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/a1legalfreelance">A1legalfreelance</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/igorgetmeabrain">Doug Badger</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/JeremyLT">Jeremy</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/fcc4b6d10c4-b540-4e2">fcc4b6d10c4-b540-4e2</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/JuniorQ">Arakhsh Q</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/Ray13">Raymond</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/hbar1st">Hanaa B.</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/lasjorg">Lasse</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/sanity">sanity or not</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/MostafaElbadry">MostafaElbadry</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/a2937">Anna</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/seopostexpert">Muhammad Subhan</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/stephenmutheu">Stephen Mutheu</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/zs_akkaya">Zeynep Serra Akkaya</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/bappyasif">A.Bappy</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/PauloRodrigues">Paulo</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/StaySilent">StaySilent</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/Klexvier">Klexvier</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/robheyays">Robert H.</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/BlindVisionMan">Marvin Hunkin</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/tracy.chacon.00">Tracy Chacon</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/bochard">bochard</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/Cody_Biggs">CODY BIGGS</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/be_happy"><em>Infinity</em></a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/ShadyHBedda">Shady H. Bedda</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/booleanmethod9">Boolean Method</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/ArielLeslie">Ariel Leslie</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/DanielHuebschmann">Head in Cloud</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/Dovb1ek">Dovb1ek</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/c0d1ng_ma5ter">c0d1ng_ma5ter</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/evaristoc">evaristoc</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/ahraitch">ahr aitch</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/jwhoisfondofit">Jay</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/AlexK">AlexanderTheDev</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/Malcolm-Harrison">Malcolm Harrison</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/Ethan1">Ethan1</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/vikramvi">Vikram Ingleshwar</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/constantcode9909">Amine (Mike)</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/nickrg">Nicolas Greenwood</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/Amunyelet-Ojala">Amunyelet-Ojala</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/anon75571083">anon75571083</a></p>
</li>
<li>
<p><a href="https://forum.freecodecamp.org/u/brendenhowlett96">Brenden Howlett</a></p>
</li>
</ul>
<h2 id="topcontributor">翻译 Top Contributor</h2>
<ul>
<li>
<p>Afonso Branco (AfonsoBranco)</p>
</li>
<li>
<p>Michael Qu (qubycn)</p>
</li>
<li>
<p>Alan Luo (iLtc)</p>
</li>
<li>
<p>Nadja Sellinat (biebricherin)</p>
</li>
<li>
<p>David Almeida (david.miguel.almeida)</p>
</li>
<li>
<p>jeigux</p>
</li>
<li>
<p>Nairobi (fanqie)</p>
</li>
<li>
<p>Ivan Forcati (IvanF)</p>
</li>
<li>
<p>Quang Nguyen (nguyendangquang126)</p>
</li>
<li>
<p>Ganebas</p>
</li>
<li>
<p>Kentaro Nareswara (K3N7)</p>
</li>
<li>
<p>Alana Maia (nicegrrrl)</p>
</li>
<li>
<p>Gustavo Birman (Gustavuspqr)</p>
</li>
<li>
<p>Nataliia Hrytsyk (nataliia.hrytsyk)</p>
</li>
<li>
<p>Filipe Oliveira (FilipeOliveira)</p>
</li>
<li>
<p>Діана Маркута (dianamarkuta)</p>
</li>
<li>
<p>Kostiantyn Krysenko (barkode)</p>
</li>
<li>
<p>v4n31aa</p>
</li>
<li>
<p>janni1288</p>
</li>
<li>
<p>Nastasia Milosev (nastasia.milosev)</p>
</li>
<li>
<p>Juan Diaz (JuanPabloDiaz)</p>
</li>
<li>
<p>Stas Kinash (stasKinash)</p>
</li>
<li>
<p>ToteM</p>
</li>
<li>
<p>Tihomir Manushev (haraGADygyl)</p>
</li>
<li>
<p>Maximo Sanchez (maxysanchez.06)</p>
</li>
<li>
<p>Сервило Галина (servilogalina)</p>
</li>
<li>
<p>Laureline Paris (LaurelineP)</p>
</li>
<li>
<p>mubinabegimxayrullayeva</p>
</li>
<li>
<p>mitegab</p>
</li>
<li>
<p>Berke Volkan (kzlpndx)</p>
</li>
<li>
<p>Dana Volovelsky (danavolovelsky)</p>
</li>
<li>
<p>Isauro Rodriguez (icaro5)</p>
</li>
<li>
<p>Yuliia Lishchuk (yuli)</p>
</li>
<li>
<p>Palak (palakkhan2002)</p>
</li>
<li>
<p>bahtiyorjonq777</p>
</li>
<li>
<p>Olha Boretska (olha.boretskaa)</p>
</li>
<li>
<p>yidev27</p>
</li>
<li>
<p>Ilona Sheremeta (lonasheremeta78)</p>
</li>
<li>
<p>Anna Shram (annashram53)</p>
</li>
<li>
<p>Anastasiia Perchyshyn (anastasijp2004)</p>
</li>
<li>
<p>Mariia Soloshenko (Mariia_S)</p>
</li>
<li>
<p>erickk (lucerile435)</p>
</li>
<li>
<p>Fran Sanabria (fransanabria)</p>
</li>
<li>
<p>Богдана Онищук (bohdanaon9001)</p>
</li>
<li>
<p>Shogo SENSUI (1000ch)</p>
</li>
<li>
<p>maysa42snow</p>
</li>
<li>
<p>Halia Senkiv (haliasenkiv)</p>
</li>
<li>
<p>Jiyoung Suh (JiyoungSuh)</p>
</li>
<li>
<p>Anairis Carballea (acarballea)</p>
</li>
<li>
<p>Johan Javier Gonzalez Perez (javiergonzalez045)</p>
</li>
<li>
<p>yosrmaalej47</p>
</li>
<li>
<p>Akram Dhib (akramdhib999)</p>
</li>
<li>
<p>Bảo Nam Trần Hoàng (kipi91212)</p>
</li>
<li>
<p>Eduarda Groehs (egroehs)</p>
</li>
<li>
<p>FlameC (anonymHe)</p>
</li>
<li>
<p>sohyun</p>
</li>
<li>
<p>Xiaoyan Zhang (Drwhooooo)</p>
</li>
<li>
<p>Gabriela Silva (gabrielaquintilho.s)</p>
</li>
<li>
<p>Fausto Chiacchietta (faustooch)</p>
</li>
<li>
<p>Seif-03</p>
</li>
<li>
<p>オメロ (homero304)</p>
</li>
<li>
<p>Msam</p>
</li>
<li>
<p>Eya (eyaaba)</p>
</li>
<li>
<p>mohamed ben haj salah (mohamedbhs7)</p>
</li>
<li>
<p>abdallah djarraya (abdallahswimmer)</p>
</li>
<li>
<p>Aylin Gümüş (aylingumus)</p>
</li>
<li>
<p>aminezribi03</p>
</li>
<li>
<p>Eloy Gutiérrez (eloy.alumnes)</p>
</li>
<li>
<p>Ameeri22</p>
</li>
<li>
<p>youssef1607</p>
</li>
<li>
<p>Pablo J Lebed (pjl1978)</p>
</li>
<li>
<p>Mergen N (mn)</p>
</li>
<li>
<p>Yuki Shibata (kyubashi)</p>
</li>
<li>
<p>ggfly666</p>
</li>
<li>
<p>Amine (ersu.amine)</p>
</li>
<li>
<p>WaifuXv</p>
</li>
<li>
<p>Jawnex</p>
</li>
<li>
<p>mamaruo</p>
</li>
<li>
<p>Eric Gigondan (Itsatsu)</p>
</li>
<li>
<p>Hamzalakoud</p>
</li>
<li>
<p>c.marget</p>
</li>
<li>
<p>Saki Basken (sbasken)</p>
</li>
<li>
<p>hashim rashid (hhashbrown)</p>
</li>
<li>
<p>yuan-minglongze</p>
</li>
<li>
<p>OKmimech</p>
</li>
<li>
<p>J.G. P.C. (kaiserpc)</p>
</li>
<li>
<p>Fatma Ajroud (faty_aj)</p>
</li>
<li>
<p>Yuna_707</p>
</li>
<li>
<p>Franklin Solar Navarrete (fsolarnavarrete)</p>
</li>
<li>
<p>zeinebBenRayana</p>
</li>
<li>
<p>Mark P. (Futuraura)</p>
</li>
<li>
<p>Ahmad Hassan (sUfi)</p>
</li>
<li>
<p>Ivrin Ivrin (Ivrin)</p>
</li>
<li>
<p>parapara0919</p>
</li>
<li>
<p>Panah (panah)</p>
</li>
<li>
<p>Mario Turtoi (MarioDev)</p>
</li>
<li>
<p>Edenilson Ulises Aguilar Diaz (UlisesDiaz0)</p>
</li>
<li>
<p>rustamdocstranslator</p>
</li>
<li>
<p>Cristian Salazar (Cristian-27)</p>
</li>
<li>
<p>IsabelaMB</p>
</li>
<li>
<p>Khalil Sassi (khalilsassi67)</p>
</li>
<li>
<p>Sorayadc</p>
</li>
<li>
<p>Franco Casafus (francocasafus22)</p>
</li>
<li>
<p>miwamiwamiwa (miwalaa)</p>
</li>
<li>
<p>franciscomelov</p>
</li>
<li>
<p>Juan Taroni (juanribeiro.taroni)</p>
</li>
<li>
<p>Alexander Liu Gao (aleliu)</p>
</li>
<li>
<p>YC liou (iop52896)</p>
</li>
<li>
<p>David Oliveira (EngDavidOlivr)</p>
</li>
<li>
<p>Campoz _ (campozzz)</p>
</li>
<li>
<p>Roberta Meyrelles (rmftelier)</p>
</li>
<li>
<p>Polina (minlaux)</p>
</li>
<li>
<p>Matheus G. Oliveira (PomboObeso)</p>
</li>
<li>
<p>Hou Bowei (houbowei)</p>
</li>
<li>
<p>Paul (ptijero)</p>
</li>
<li>
<p>Danilo Parada Garcés (dparada.sistemas)</p>
</li>
<li>
<p>Ana_Writer</p>
</li>
<li>
<p>Nathalia Oliveira (royalpython)</p>
</li>
<li>
<p>Amir (Amir_lvx)</p>
</li>
<li>
<p>Eltaj Mammadzada (eltajmammadzada)</p>
</li>
<li>
<p>Oliver Loza (THE_G3NES1S)</p>
</li>
<li>
<p>Jakhongir Murtazaev (jakhongir.murtazayev)</p>
</li>
<li>
<p>Siyana Zdravkova (BlueButterflies)</p>
</li>
<li>
<p>Vindishel (vindishel)</p>
</li>
<li>
<p>Daniel Jimenez (danjim82)</p>
</li>
<li>
<p>hk7math</p>
</li>
<li>
<p>KAWPHUNMAN</p>
</li>
<li>
<p>wdthor</p>
</li>
<li>
<p>Snow sita2 (EnmanuelTorres)</p>
</li>
<li>
<p>Aldo Vanegas (AldoLara)</p>
</li>
<li>
<p>nmo-genio</p>
</li>
<li>
<p>Pedro Daniel (pedrodanielgomes)</p>
</li>
<li>
<p>sadnessasha</p>
</li>
<li>
<p>Aby Prastya Palgunadi (arcanaxvi)</p>
</li>
<li>
<p>Halifolium</p>
</li>
<li>
<p>juan jose (Juanx64)</p>
</li>
<li>
<p>Henry Richard Flores Bazurto (hflores10)</p>
</li>
<li>
<p>Leah</p>
</li>
<li>
<p>Emma (emmaa5)</p>
</li>
<li>
<p>ANVAR ZIYODOV (ziyodovanvar1999)</p>
</li>
<li>
<p>dharris296</p>
</li>
<li>
<p>Juan Esteban Montoya Marín (montoyajuanes11)</p>
</li>
<li>
<p>Yiming Sun (sunyiming008)</p>
</li>
<li>
<p>Fauzi Kurniawan (kurniawan26)</p>
</li>
<li>
<p>Royyan Ahmad Zaydan (Kasehito)</p>
</li>
<li>
<p>Mikadifo</p>
</li>
<li>
<p>thelooter</p>
</li>
<li>
<p>Berkcan Gümüşışık (berkcangumusisik)</p>
</li>
<li>
<p>Stephen Mutheu (stephenmutheu)</p>
</li>
<li>
<p>athen</p>
</li>
<li>
<p>Wajahat (syedmuhammadwajahathusain)</p>
</li>
<li>
<p>Tanish Chauhan (tanishc4444)</p>
</li>
<li>
<p>Satya900</p>
</li>
<li>
<p>Luis V. (lvalderramavergara)</p>
</li>
<li>
<p>Sharvio</p>
</li>
<li>
<p>HibouDev</p>
</li>
<li>
<p>dTM99</p>
</li>
<li>
<p>wuzzjohn</p>
</li>
<li>
<p>Gavin Xu (gavinxu2)</p>
</li>
<li>
<p>ProjektMing</p>
</li>
<li>
<p>Saidkamol Saidjamolov (saidkamolxon)</p>
</li>
<li>
<p>beta filip (betafilip)</p>
</li>
<li>
<p>Deborah Porchia (deborah98)</p>
</li>
<li>
<p>teddy_ye</p>
</li>
<li>
<p>Rommel Hindap (rommel.b.hindap)</p>
</li>
<li>
<p>Jesús Lautaro Careglio Albornoz (JLCareglio)</p>
</li>
<li>
<p>SergioBlancoFtns</p>
</li>
<li>
<p>Nathalie Bour (nathalie.bour)</p>
</li>
<li>
<p>Qingfeng Huang (darrenhqf)</p>
</li>
<li>
<p>Omar (omar.fanzeres)</p>
</li>
<li>
<p>Atsushi Hatakeyama (atsushi729)</p>
</li>
<li>
<p>Diego Fierro (diegoefierro)</p>
</li>
<li>
<p>IsaRO (IsaR0d)</p>
</li>
<li>
<p>Abdulbosit Tuychiev (abdulbosit19980204)</p>
</li>
<li>
<p>NG KA YEE (hkscsheph)</p>
</li>
<li>
<p>Floman Dizwit (Hockman)</p>
</li>
<li>
<p>Karel Vanhelden (karelvanhelden)</p>
</li>
<li>
<p>khay56</p>
</li>
<li>
<p>Dostonbek Matyakubov (doston12)</p>
</li>
<li>
<p>MarcoGeldenhuis</p>
</li>
<li>
<p>immeteor2</p>
</li>
<li>
<p>Freedom Fighter (1543431a)</p>
</li>
<li>
<p>Ibn Hosain (ibnhosain014)</p>
</li>
<li>
<p>Vairus (e-oannis)</p>
</li>
<li>
<p>Elio Fang</p>
</li>
<li>
<p><a href="https://x.com/0x99Ethan3">YiWei</a></p>
</li>
<li>
<p><a href="https://x.com/TsukistarCN">Tsukistar</a></p>
</li>
<li>
<p>luojiyin</p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.linkedin.com/in/qingfeng-huang">Qingfeng Huang</a></p>
</li>
<li>
<p>wendy chen</p>
</li>
<li>
<p>zhizhan</p>
</li>
<li>
<p>HeZean</p>
</li>
<li>
<p>Ivan Forcati</p>
</li>
<li>
<p>Andrea Sisti</p>
</li>
<li>
<p><a href="https://x.com/Xuemei525">彭雪梅</a></p>
</li>
</ul>
<h2 id="youtubetopcontributor">YouTube Top Contributor</h2>
<ul>
<li>
<p><a href="https://github.com/s1ngs1ng">S1ng S1ng</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.linkedin.com/in/leoncarlo/">Carlos León</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@LuisCanary">Luis Canary</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@programacaocomramon">Ramon Rodrigues</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/github.com/jamesgpearce">James Pearce</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@officialtatevaslanyan">Tatev Aslanyan</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@EricWTech">Eric Tech</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@RivaanRanawat">Rivaan Ranawat</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@KhanamCoding">Khaiser Khanam</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@vincibits">Paulo Dichone</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@HiteshCodeLab">Hitesh Choudhary</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@EamonnCottrell">Eamonn Cottrell</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@harshbhatt7585">Harsh Bhatt</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@radu">Radu Mariescu-Istodor</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@3CodeCampers">Imad Saddik</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@EmbarkX">Faisal Memon</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@JSLegendDev">JSLegendDev</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@ExamProChannel">Andrew Brown</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@DaveGrayTeachesCode">Dave Gray</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@asabretech">Ebenezer Asabre</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@the-lisper">Alberto Lerda</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@deeplearningexplained">Yacine Mahdid</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@codeafuture">Alen Omeri</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@tungabayrak9765">Tunga Bayrak</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@CodingCleverly">Haris Iftikhar</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/rdali.github.io/">Rola Dali</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@OmarMAtef">Omar M. Atef</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/github.com/stevenGarciaDev">Steven Garcia</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@TrickSumo">Rishi Kumar Tiwari</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@coleblender">Cole Blender</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@AlterYourEnglish">Borys Cherednychenko</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@haidermalik3402">Haider Malik</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@excel.withgrant">Grant Huang</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@vukrosic">Vuk Rosić</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@LearnQtGuide">Daniel Gakwaya</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/github.com/BrijenMakwana">Brijen Makwana</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@TheCodeholic">Zura Sekhniashvili</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@turingtimemachine">Vladimirs Hisamutdinovs</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@tapasadhikary">Tapas Adhikary</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@richardtopchii">Richard Topchii</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@code-with-abel">Abel Gideon</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@MatkatMusic">Chuck Schiemeyer</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@programmingwithalex.585">Alexandru Cristian</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@masterspanishacademy">Virginia Ocana</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.linkedin.com/in/vaibhav-mehra-main/">Vaibhav Mehra</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/optimusprime09012004@gmail.com">Kshitij Sharma</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.dotnetmastery.com">Bhrugen Patel</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@programmingoceanacademy">Mohammad Fahd Abrah</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@ThePyCoach">Frank Andrade</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@ChadsPrep">Chad McAllister</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@ErikYuzwa">Erik Yuzwa</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@dswithbappy">Bappy Ahmed</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@Programming-Fluency">Noor Fakhry</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@AyushSinghSh">Ayush Singh</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@ever-greg">Gregory Kirchoff</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@algo.monster">Sheldon Chi</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@GlitchyDevs">Muhammad Omar Al Najjar</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@talltalksfromashortlady2798">Chumki Biswas</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/c/AlvinTheProgrammer">Alvin Zablan</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@structuredcs">Qiang Hao</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@datasciencewithmarco">Marco Peix</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@logicBaseLabs">Sumit Saha</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.linkedin.com/in/yilmazalaca">Yılmaz Alaca</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@robotbobby9">Bobby Roe</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/x.com/wagslane">Lane Wagner</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/%E2%80%AA@mobidevtalk">Shad Rayhan Mazumder</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@DotNetHow">Ervis Trupja</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/github.com/vivekkalyanarangan30">Vivek Kalyanarangan</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@cs50">David J. Malan</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.linkedin.com/in/leoncarlo/">Carlos Leon</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@BlossomBuild">Carlos Valentin</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@DestinationFAANG">Parth Vyas</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@codewithmuhammadabdullah">Muhammad Abdullah</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@andrewwoan">Andrew Woan</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/freecodecamp-2025-top-contributors/www.youtube.com/@AlexGordonHiFi">Alex Gordon</a></p>
</li>
<li>
<p><a href="http://youtube.com/@TwoWaysMath">Karol Kurek</a></p>
</li>
</ul>
<h2 id="topcontributor">专栏 Top Contributor</h2>
<ul>
<li>
<p><a href="https://freecodecamp.org/news/author/manishshivanandhan">Manish Shivanandhan</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/nsavant">Nikheel Vishwas Savant</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/appinisurya">Surya Teja Appini</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/tayo4christ">OMOTAYO OMOYEMI</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Casmir">Casmir Onyekani</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Chukwudinweze">Chukwudi Nweze</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/GerCocca">German Cocca</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Shejan-Mahamud">Shejan Mahamud</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/hew">Hew Hahn</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/curiousmoshe">Moshe Siegel</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/markm208">Mark Mahoney</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/atuoha">Atuoha Anthony</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/balapriyac">Bala Priya C</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ashutoshkrris">Ashutosh Krishna</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/hiteshchauhan2023">Hitesh Chauhan</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/tiredmahnoor">Mah Noor</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/andrewbaisden">Andrew Baisden</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/trayalex812">Alex Tray</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/gkoos">Gabor Koos</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Abhidave">Abhijeet Dave</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/sumitsaha">Sumit Saha</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Tech-On-Diapers">Opaluwa Emidowojo</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/oluwatobiss">Oluwatobi Sofela</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/sholajegede">Shola Jegede</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/michaelyuan">Michael Yuan</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ChisomUma123">Chisom Uma</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/heywisdom">Wisdom Usa</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/kuriko">Kuriko Iwai</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ceddlyburge">Cedd Burge</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/CaesarSage">Destiny Erhabor</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/leomofthings">Ayodele Aransiola</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Koded001">Temitope Oyedele</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/emdadulislam">Emdadul Islam</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Tioluwani">Oyedele Tioluwani</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/mehtasoham">Soham Mehta</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Agnes28">Agnes Olorundare</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/atapas">Tapas Adhikary</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/chiragagrawal">Chirag Agrawal</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ATechAjay">Ajay Yadav</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/mayur9210">Mayur Vekariya</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Sharvin26">Sharvin Shah</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/adejumo">Adejumo Ridwan Suleiman</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/sravankaruturi">Sravan Karuturi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/wagslane">Lane Wagner</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/nitheeshp">Nitheesh Poojary</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/almamohapatra">Alma Mohapatra</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/orimdominic">Orim Dominic Adah</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/arunachalamb">Arunachalam B</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Ayush01Mishra">AYUSH MISHRA</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/shricodev">Shrijal Acharya</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Oliverkrane">Ikegah Oliver</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Jongbo">Olaleye Blessing</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ifycodes99">Ifeoma Udu</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Tobilyn77">Oluwatobi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/oluwaseunoladeji">Oladeji Oluwaseun</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/tarunsinghofficial">Tarun Singh</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/grantdotdev">Grant Riordan</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/asfakahmed">Asfak Ahmed</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/LeeRenJie">Tech With RJ</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/timkleier">Tim Kleier</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/CodeHemaa">Ophy Boamah</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ezinnecodes">EZINNE ANNE EMILIA</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Codinghappiness">Happiness Omale</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/bertao">Pedro</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Lonercode">Amanda Ene Adoyi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/arunshanmugamkumar">Arun Shanmugam Kumar</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/andrewezeani">Andrew Ezeani</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Ajay074">Ajay Kalal</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Ijay">Ijeoma Igboagu</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/prankurpandeyy">Prankur Pandey</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/augustinealul">Augustine Alul</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/sanjayxr">Sanjay R</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/adiatiayu">Ayu Adiati</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/cardstdani">Daniel García Solla</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Clifftech">Isaiah Clifford Opoku</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ryan-michael-kay">Ryan Michael Kay</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/olanetsoft">Idris Olubisi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/wittycircuitry">Aditya Vikram Kashyap</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/brkln">brooklyn</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/TemiTope1">Tope Fasasi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ilknureren">Ilknur Eren</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/sohamstars">Soham Banerjee</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/vaheaslanyan">Vahe Aslanyan</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/tatevaslanyan">Tatev Aslanyan</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/henrywinnerman">Henry Adepegba</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ThatCoolGuy">Oluwadamilola Oshungboye</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/shrutikapoor">Shruti Kapoor</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/theladybella">Mfonobong Umondia</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Spruce">Spruce Emmanuel</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Derekvibe">Okoro Emmanuel Nzube</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/rajumanoj">Raju Manoj</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/udemezue">Udemezue John</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/lulunwenyi">Oluchi Nwenyi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/toobaj">Tooba Jamal</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/nwanduka">Victoria Nduka</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Nene23">Nneoma Uche</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/michaelikoko">Michael Ikoko</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Jude-Olowo">Olowo Jude</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Ateev">Ateev Duggal</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Oluwadamisi">Oluwadamisi Samuel</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/menghnani">Mohit Menghnani</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/scriptedBytes">Brandon Wozniewicz</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/huhuhang">Hang Hu</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/mrufai">Rufai Mustapha</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/LolaVictoria">Damilola Oniyide</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/HijabiCoder">Fatuma Abdullahi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/montasser1988">Montasser Mossallem</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/gatwirival">valentine Gatwiri</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/SmoothTech">Timothy Olanrewaju</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/gitgithan">Han Qi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Omah">Eti Ijeoma</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Yazdun">Yazdun</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/desoga">deji adesoga</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/the_BrianB">Brian Barrow</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/chidiadi01">Chidiadi Anyanwu</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/codelikeandrew">Andrew Maksimchenko</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/mcasari">Mario Casari</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Balajeeasish">Balajee Asish Brahmandam</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/woai3c">Gordan Tan</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/tildaudufo">Tilda Udufo</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/rahulgupta32">Rahul gupta</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/edae">Eda Eren</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/josiahadesola">Josiah Adesola</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/smarttester">Venkata Sai Sandeep</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/tiagomonteiro">Tiago Capelo Monteiro</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Preston56">Preston Osoro</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/TheAnkurTyagi">Ankur Tyagi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/justanothertechlead">Ben</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/askvikram">Vikram Aruchamy</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/dhruv-007">Dhruv Prajapati</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/onukwilip">Prince Onukwili</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/AdeboyeDN">Daniel Adeboye</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/kanand">Kumar Anand</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/lucasgarcez">Lucas</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ec001">evaristo.c</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/officialrajdeepsingh">Rajdeep Singh</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/chaitanyarahalkar">Chaitanya Rahalkar</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/MahamCodes">Maham Codes</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/divyasaratchandran">Divya Valsala Saratchandran</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/joanayebola">Joan Ayebola</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/omerros">Omer Rosenbaum</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Nazneen758">Nazneen Ahmad</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/KunalN25">Kunal Nalawade</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/sdranju">Shamsuddoha Ranju</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/anamol-rajbhandari">Anamol Rajbhandari</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/suleolanrewaju">Sule-Balogun Olanrewaju</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Eccentric-">Sara Jadhav</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/LifeofDan-EL">Daniel Anomfueme</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Michael-para">Michael Para</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Ellabee">Elabonga Atuo</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/azubuikeduru">Azubuike Duru</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/AdalbertPungu">Adalbert Pungu</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/gunkev">Kevine Nzapdi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/hamdaan">Hamdaan Ali</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/AbdullahInBytes">Abdullah Salaudeen</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/RAHULISM">Rahul</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Ayobami6">Alaran Ayobami</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/omoladeekpeni">Omolade Ekpeni</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/rwalters">Rob Walters</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/viv1">Vivek Sahu</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Xtephen">oghenekparobo Stephen</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/psmohammedali">P S Mohammed Ali</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/gursimar">Gursimar Singh</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Olabisi09">Olabisi Olaoye</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/de">David Asaolu</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/initialcommit">Jacob Stopak</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Yoma">Emore Ogheneyoma Lawrence</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/hunor">Hunor Márton Borbély</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/techwithpraisejames">Praise James</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/pltvs">Alex Pliutau</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/tanishkamakode">Tanishka Makode</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/SonyaMoisset">Sonya Moisset</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/vkweb">Vivek Agrawal</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/syedamahamfahim">Syeda Maham Fahim</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/chiderahumphrey">Chidera Humphrey</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/DoableDanny">Danny</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/stefanmuzyka">Stefan Muzyka</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/samhitharamaprasad">Samhitha Rama Prasad</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/nitinfab">Nitin Sharma</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/codewithshahan">Programming with Shahan</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/jpromanonet">Juan P. Romano</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/anjanbaradwaj">Anjan Baradwaj</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/manocormen">Manoel</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/mkbadeniyi">Kayode Adeniyi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/MuhToyyib">Akande Olalekan Toheeb</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/dbclinton">David Clinton</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/josevnz">Jose Vicente Nunez</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/joeattardi">Joe Attardi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/mihailgaberov">Mihail Gaberov</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/ashimi0x">Ashimi0x</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Zubs">Zubair Idris Aweda</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/IbrahimOgunbiyi">Ibrahim Ogunbiyi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/svlorman">Svitlana Lorman</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/timmy471">Ayantunji Timilehin</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/imkrishnasarathi">Krishna Sarathi Ghosh</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/Daiveed">David Jaja</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/francisihe">Francis Ihejirika</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/marco-venturi">Marco Venturi</a></p>
</li>
<li>
<p><a href="https://freecodecamp.org/news/author/nyayicfanny">Fanny Nyayic</a></p>
</li>
</ul>
<h2 id="discordtopcontributor">Discord Top Contributor</h2>
<ul>
<li>
<p>hana-banana</p>
</li>
<li>
<p>Science99</p>
</li>
<li>
<p>Razzle Dazzle</p>
</li>
<li>
<p>jeremylt (他/他们)</p>
</li>
<li>
<p>Makka Pakka jOoJ</p>
</li>
<li>
<p>bradtaniguchi</p>
</li>
<li>
<p>ʇɹǝqɯoɥɹ</p>
</li>
<li>
<p>Versailles</p>
</li>
<li>
<p>CapslockHero 🎃</p>
</li>
<li>
<p>plamoni</p>
</li>
<li>
<p>xCoffeeMan</p>
</li>
<li>
<p>Dylan</p>
</li>
<li>
<p>QC Failed (Brandon)</p>
</li>
<li>
<p>minjo70</p>
</li>
<li>
<p>tgrtim</p>
</li>
<li>
<p>Yu14</p>
</li>
<li>
<p>localhost</p>
</li>
<li>
<p>ArielLeslie</p>
</li>
<li>
<p>Wayloe</p>
</li>
<li>
<p>Hordian</p>
</li>
<li>
<p>Anna</p>
</li>
<li>
<p>Starbreeze</p>
</li>
<li>
<p>supertanno</p>
</li>
<li>
<p>Hermit</p>
</li>
<li>
<p>Aakash</p>
</li>
<li>
<p>Ganesh</p>
</li>
<li>
<p>Zino</p>
</li>
<li>
<p>Cristina</p>
</li>
<li>
<p>Pantalonians</p>
</li>
<li>
<p>Cy4er</p>
</li>
<li>
<p>himonshuuu</p>
</li>
<li>
<p>Dumb ninja</p>
</li>
<li>
<p>Kiseki 奇跡</p>
</li>
<li>
<p>Sebastian</p>
</li>
<li>
<p>alpox</p>
</li>
<li>
<p>BasCat</p>
</li>
</ul>
<p>再次说明，这些只是参与 freeCodeCamp 社区的成千上万的人中最活跃的一些。</p>
<p>如果你有兴趣以开源贡献者的身份参与 freeCodeCamp 社区，我建议你<a href="https://contribute.freecodecamp.org/#/">阅读我们的贡献者指南</a>，并加入我们的<a href="https://discord.gg/KVUmVXA">贡献者 Discord 聊天室</a>。</p>
<p>再次感谢，祝编码愉快。🏕️</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
