这一课我们把 resume-generator.ts 的 98 行代码逐行读完,把前面 7 课学到的知识全部串起来。

第 8 课:完整项目实战


本课目标

这一课我们把 resume-generator.ts 的 98 行代码逐行读完,把前面 7 课学到的知识全部串起来。

完整源码(带注释版)

bash
/**
 * Resume Generator using Claude Agent SDK
 *
 * 用 web search 搜索一个人的信息,然后自动生成一份 .docx 简历
 *
 * 用法: npx tsx resume-generator.ts "人名"
 */

// ========== 第一部分:导入依赖 ==========

import { query } from '@anthropic-ai/claude-agent-sdk';
// query:SDK 的核心函数,"给 AI 一个任务,AI 去完成"(第 3 课)

import * as fs from 'fs';
// fs:Node.js 文件系统模块,用来检查文件是否存在

import * as path from 'path';
// path:路径处理,拼接文件路径用的


// ========== 第二部分:系统提示词 ==========

const SYSTEM_PROMPT = `You are a professional resume writer.
Research a person and create a 1-page .docx resume.

WORKFLOW:
1. WebSearch for the person's background (LinkedIn, GitHub, company pages)
2. Create a .docx file using the docx library

OUTPUT:
- Script: agent/custom_scripts/generate_resume.js
- Resume: agent/custom_scripts/resume.docx

PAGE FIT (must be exactly 1 page):
- 0.5 inch margins, Name 24pt, Headers 12pt, Body 10pt
- 2-3 bullet points per job, ~80-100 chars each
- Max 3 job roles, 2-line summary, 2-line skills`;
// 系统提示词的四大块:角色、流程、输出、约束(第 6 课)


// ========== 第三部分:核心函数 ==========

async function generateResume(personName: string) {
  console.log(`\n📝 Generating resume for: ${personName}\n`);
  console.log('='.repeat(50));

  // 确保输出目录存在
  const outputDir = path.join(process.cwd(), 'agent', 'custom_scripts');
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }
  // ↑ 提前创建好 agent/custom_scripts/ 目录
  //   这样 AI 写代码时可以直接写到这里

  // 用户提示词:每次任务不同的具体指令
  const prompt = `Research "${personName}" and create a professional
                  1-page resume as a .docx file. Search for their
                  professional background, experience, education,
                  and skills.`;

  console.log('\n🔍 Researching and creating resume...\n');

  // ====== 关键调用:query() ======
  const q = query({
    prompt,        // 用户提示词
    options: {
      maxTurns: 30,    // 最多 30 轮操作(第 3 课)
      cwd: process.cwd(),  // 工作目录
      model: 'sonnet',     // 用 Sonnet 模型
      allowedTools: [      // 工具箱(第 4 课)
        'Skill',       // 读技能文件
        'WebSearch',   // 搜索
        'WebFetch',    // 抓取网页
        'Bash',        // 执行命令
        'Write',       // 写文件
        'Read',        // 读文件
        'Glob'         // 查找文件
      ],
      settingSources: ['project'],  // 加载 .claude/skills/(第 5 课)
      systemPrompt: SYSTEM_PROMPT,  // 系统提示词(第 6 课)
    },
  });

  // ====== 处理消息流 ======
  for await (const msg of q) {

    // 处理 AI 发来的消息
    if (msg.type === 'assistant' && msg.message) {
      for (const block of msg.message.content) {

        // AI 说了一段话
        if (block.type === 'text') {
          console.log(block.text);
        }

        // AI 要用工具
        if (block.type === 'tool_use') {
          if (block.name === 'WebSearch' &&
              block.input &&
              typeof block.input === 'object' &&
              'query' in block.input) {
            // 搜索工具——特殊处理,显示搜索关键词
            console.log(`\n🔍 Searching: "${block.input.query}"`);
          } else {
            // 其他工具——只显示工具名
            console.log(`\n🔧 Using tool: ${block.name}`);
          }
        }
      }
    }

    // 处理工具返回的结果
    if (msg.type === 'result') {
      if (msg.subtype === 'tool_result') {
        const resultStr = JSON.stringify(msg.content).slice(0, 200);
        console.log(`   ↳ Result: ${resultStr}${resultStr.length >= 200 ? '...' : ''}`);
        // ↑ 只显示结果的前 200 个字符,避免刷屏
      }
    }
  }

  // ====== 检查结果 ======
  const expectedPath = path.join(
    process.cwd(), 'agent', 'custom_scripts', 'resume.docx'
  );
  if (fs.existsSync(expectedPath)) {
    console.log('\n' + '='.repeat(50));
    console.log(`📄 Resume saved to: ${expectedPath}`);
    console.log('='.repeat(50) + '\n');
  } else {
    console.log('\n❌ Resume file was not created. Check the output above for errors.');
  }
}


// ========== 第四部分:入口 ==========

const personName = process.argv[2];
// 从命令行参数获取人名
// npx tsx resume-generator.ts "Elon Musk"
//                              ↑ 这就是 process.argv[2]

if (!personName) {
  console.log('Usage: npx tsx resume-generator.ts "Person Name"');
  console.log('Example: npx tsx resume-generator.ts "Jane Doe"');
  process.exit(1);
}

generateResume(personName).catch(console.error);
// 启动!

四个部分总览

graph TD A["第一部分:导入依赖<br>import query + fs + path"] --> B["第二部分:系统提示词<br>SYSTEM_PROMPT = 角色 + 流程 + 输出 + 约束"] B --> C["第三部分:核心函数 generateResume"] C --> C1["准备输出目录"] C1 --> C2["调用 query"] C2 --> C3["处理消息流(显示进度)"] C3 --> C4["检查结果文件"] C4 --> D["第四部分:入口<br>读取命令行参数 - 调用 generateResume"]

消息流处理详解

for await 循环里的消息处理是理解 Agent 行为的关键。让我们用一个具体例子来看 AI 实际发出的消息序列:

bash
时序图:生成 "Elon Musk" 简历

你的程序                        AI (Claude)
   │                              │
   │── query(prompt) ──────────>  │
   │                              │
   │  <─── assistant/text ─────── │  "我来搜索 Elon Musk 的信息"
   │  <─── assistant/tool_use ─── │  WebSearch("Elon Musk LinkedIn")
   │  <─── result/tool_result ─── │  搜索结果...
   │                              │
   │  <─── assistant/tool_use ─── │  WebSearch("Elon Musk education")
   │  <─── result/tool_result ─── │  搜索结果...
   │                              │
   │  <─── assistant/tool_use ─── │  Skill("docx")
   │  <─── result/tool_result ─── │  SKILL.md 内容...
   │                              │
   │  <─── assistant/tool_use ─── │  Read("docx-js.md")
   │  <─── result/tool_result ─── │  docx-js.md 内容...
   │                              │
   │  <─── assistant/text ─────── │  "信息收集完毕,开始生成简历"
   │  <─── assistant/tool_use ─── │  Write("generate_resume.js")
   │  <─── result/tool_result ─── │  文件已创建
   │                              │
   │  <─── assistant/tool_use ─── │  Bash("node generate_resume.js")
   │  <─── result/tool_result ─── │  "Resume generated successfully!"
   │                              │
   │  <─── assistant/text ─────── │  "简历已生成,保存在..."
   │                              │

实际运行一次

让我们真正跑一次,并关注每个阶段:

code
npx tsx resume-generator.ts "Tim Cook"

你会看到类似的输出:

bash
📝 Generating resume for: Tim Cook
==================================================

🔍 Researching and creating resume...

🔍 Searching: "Tim Cook professional background CEO Apple"
   ↳ Result: [{"type":"text","text":"Search results for Tim Cook..."}]

🔍 Searching: "Tim Cook education Auburn University Duke MBA"
   ↳ Result: [{"type":"text","text":"Tim Cook received his Bachelor..."}]

🔧 Using tool: Skill
   ↳ Result: [{"type":"text","text":"# DOCX creation, editing..."}]

🔧 Using tool: Read
   ↳ Result: [{"type":"text","text":"# DOCX Library Tutorial..."}]

I'll now create the resume using the docx library.

🔧 Using tool: Write
   ↳ Result: [{"type":"text","text":"File written successfully"}]

🔧 Using tool: Bash
   ↳ Result: [{"type":"text","text":"Resume generated successfully!"}]

==================================================
📄 Resume saved to: /path/to/agent/custom_scripts/resume.docx
==================================================

从代码到完整 Agent 的思维转变

写完这个项目之后,你会发现一个重要的思维转变:

code
传统编程思维:
  "我要写代码去搜索 → 写代码处理数据 → 写代码生成文件"
  程序员负责每一步的逻辑

Agent 思维:
  "我设计好 AI 的角色和规则 → 给它配好工具 → 让它自己搞定"
  程序员负责设计框架,AI 负责执行

你的 98 行代码做了什么?

你做的 AI 做的
设计系统提示词 决定搜什么关键词
选择要用的工具 判断搜索结果是否够用
指定输出路径和格式 写出完整的 JS 代码
处理消息流显示进度 执行代码并修复错误
检查文件是否生成 确保排版符合要求

你设计了"规则",AI 做了"实事"。

本课小结

  • 整个项目只有 98 行代码,分为 4 个部分
  • 核心逻辑就是:调用 query() → 处理消息流 → 检查结果
  • 消息流是理解 Agent 行为的关键窗口
  • 从"写逻辑"到"设计规则"是 Agent 编程的思维转变

课后练习

  1. 跑三次同一个人名,看看 AI 每次生成的代码和简历是否不同
  2. 在消息流处理里加一个计数器,看看 AI 一共用了多少轮才完成任务
  3. 尝试把代码改成接受两个参数:人名和输出文件名(不一定叫 resume.docx)

沿着当前专题继续,或返回课程目录重新整理阅读顺序。

返回课程目录