温嘉琪 / BUILDING SOMETHING FUN

数据持久化,工作流的重新设计:用n8n来自动化找工作+写cover letter(3)

在构建求职流程自动化系统的过程中,我遇到了一个常见但又容易被忽视的问题:数据在处理流程中丢失了关键信息。本文将详细记录我如何诊断并解决这个问题,从根本上优化了整个系统架构,希望能帮助到其他正在构建类似自动化流程的开发者。

问题背景

我正在使用n8n构建一个求职流程自动化系统,主要功能包括:

  1. 从LinkedIn抓取职位信息
  2. 使用AI匹配用户简历与职位
  3. 发送包含匹配结果的推荐邮件
  4. 用户可点击邮件中的按钮生成Cover Letter

然而,我发现了一个关键问题:在数据处理过程中,完整的职位描述信息被丢失了。这导致系统无法生成高质量的Cover Letter,因为生成过程缺少了关键的职位细节信息。

问题诊断

通过分析工作流,我发现问题出在数据流设计上:

  1. AI Agent节点接收职位数据,但只输出简化信息(公司名、职位名、URL等)
  2. 当用户点击"Generate Cover Letter"时,系统只能访问这些简化信息
  3. 系统尝试使用URL重新抓取职位详情,但这种方法不稳定且效率低

这种设计存在的问题是:过早地简化了数据,丢失了下游功能所需的关键信息

数据丢失问题可视化.svg

解决方案:改变数据流架构

经过分析,我采用了"数据存储与处理分离"的架构模式来解决这个问题,完整步骤如下:

1. 将职位数据存储到数据库

首先,我决定在AI分析前将完整的职位数据存储到Supabase数据库:

// 将抓取的职位数据存储到Supabase
const jobsToStore = $input.all()[0].json.Jobs.map(job => ({
  company: job.company_name || "Unknown Company",
  title: job.job_title || "Unnamed Position",
  url: job.apply_url || job.linkedin_url || "",
  full_description: job.description || "",
  requirements: job.requirements || [],
  location: job.location || "",
  flexibility: determineFlexibility(job.description || ""),
  salary_range: extractSalaryInfo(job.description || "") || "Not specified"
}));

// 返回处理后的数据供Supabase节点使用
return [{ json: { jobsToStore } }];

这样,每个职位都会在数据库中分配一个唯一ID,并保存完整信息。

2. 修改AI Agent提示

原始的AI Agent提示使用{{ $json.Jobs }}引用职位数据,但存储到Supabase后数据结构发生了变化。我采用了预处理节点将数据转换回原始格式:

// 函数节点代码:将Supabase结果转换为原始格式
const jobsFromSupabase = $input.all().map(item => item.json);

// 转换为原始格式,确保字段名匹配原始格式
const processedJobs = jobsFromSupabase.map(job => ({
  id: job.id,  // 数据库ID
  company_name: job.company,
  job_title: job.title,
  description: job.full_description,
  linkedin_url: job.url,
  location: job.location,
  // 其他必要字段...
}));

// 返回适合AI节点处理的格式
return { json: { Jobs: processedJobs } };

3. 修改AI Agent输出

更新AI Agent提示,确保输出包含数据库ID:

Output Requirements:
You must return a structured list containing exactly 8 job recommendations. For each job, provide:

- database_id: The unique ID of the job in the database (CRITICAL - must be included).
- Company Name: The name of the hiring company.
- Job Title: The official title of the position.
...

4. 更新邮件模板

修改邮件模板中的按钮链接,使用数据库ID而非URL:

<!-- 修改前 -->
<a href="mailto:[email protected]?subject=GenerateCL_{{$json.output[0]['Application URL']}}&body=...">

<!-- 修改后 -->
<a href="mailto:[email protected]?subject=GenerateCL_{{$json.output[0]['database_id']}}&body=...">

5. 实现处理用户操作的补充工作流

我选择使用Webhook实现用户交互,替代原本的邮件触发方案:

<!-- 链接到Webhook -->
<a href="http://localhost:5678/webhook-test/2f32a889-c6d8-4dda-a550-71d963063aa7?jobId={{$json.output[0]['database_id']}}" target="_blank">Generate Cover Letter</a>

然后创建一个Webhook触发的工作流,处理用户请求:

  1. 接收包含jobId参数的Webhook请求
  2. 使用jobId从Supabase查询完整职位信息
  3. 获取用户简历
  4. 使用AI生成Cover Letter
  5. 返回HTML页面显示结果
// Webhook响应HTML生成示例
const coverLetter = $input.item.json.text || "Error generating cover letter";
const company = $node["Supabase"].json.company || "";
const position = $node["Supabase"].json.title || "";

const html = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Your Cover Letter for ${position} at ${company}</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        /* 更多样式... */
    </style>
</head>
<body>
    <div class="container">
        <h1>Cover Letter for ${position} at ${company}</h1>

        <pre id="coverLetterText">${coverLetter}</pre>

        <div class="actions">
            <button class="copy-btn" onclick="copyToClipboard()">Copy to Clipboard</button>
            <button class="download-btn" onclick="downloadAsTxt()">Download as TXT</button>
        </div>
    </div>

    <script>
        function copyToClipboard() {
            const text = document.getElementById('coverLetterText').innerText;
            navigator.clipboard.writeText(text)
                .then(() => alert('Cover letter copied to clipboard!'))
                .catch(err => console.error('Error copying text: ', err));
        }

        function downloadAsTxt() {
            const text = document.getElementById('coverLetterText').innerText;
            const element = document.createElement('a');
            element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
            element.setAttribute('download', 'Cover_Letter_${company}_${position}.txt');
            element.style.display = 'none';
            document.body.appendChild(element);
            element.click();
            document.body.removeChild(element);
        }
    </script>
</body>
</html>
`;

return {
  json: {
    responseCode: 200,
    headers: {
      'Content-Type': 'text/html'
    },
    response: html
  }
};

求职流程自动化系统架构对比.svg

遇到的挑战与解决方法

挑战1:Code节点中的"没有触发器"错误

当我添加数据预处理节点时,遇到了"Connect a trigger to run this node"错误。这个错误通常有几个可能的原因:

  1. 工作流没有起始触发器节点
  2. 节点之间的连接不完整
  3. 尝试单独执行节点而非整个工作流
  4. 节点配置问题

解决方法

  • 确保从触发器节点到Supabase节点再到Code节点有完整的连接路径
  • 添加Debug节点诊断数据流
// 使用Code节点而非Function节点
const items = $input.all();
let jobsFromSupabase = [];

// 提取每个项目的json数据
for (const item of items) {
  if (item.json) {
    jobsFromSupabase.push(item.json);
  }
}

// 处理数据并返回
return [{
  json: {
    Jobs: processedJobs
  }
}];

挑战2:数据结构变化导致的引用问题

当数据流经Supabase节点后,原始的{{ $json.Jobs }}引用不再有效。

解决方法

  1. 添加Debug节点查看实际数据结构
  2. 使用预处理节点将数据转换为AI节点期望的格式
  3. 确保所有字段名称与原始格式匹配

挑战3:本地开发环境下的Webhook测试

使用localhost地址的Webhook只能在运行n8n的同一台计算机上工作。

解决方法

  1. 开发阶段:保持使用localhost URL进行测试
  2. 确保在同一台运行n8n的计算机上打开邮件并点击链接
  3. 生产环境部署前:将n8n部署到公共可访问的服务器,更新Webhook URL

关键经验教训

  1. 避免过早简化数据:在处理复杂数据时,保留完整原始数据直到确实不再需要它们的环节。
  2. 使用"数据通过引用而非值"的原则:使用唯一ID引用数据,而非传递完整内容,这样既保证了信息完整性又兼顾了处理效率。
  3. 分离数据存储与处理:将存储层与处理逻辑分开,使系统更灵活、更容易扩展和维护。
  4. 彻底测试数据流转换:每当数据经过转换节点,都应该使用Debug节点验证结构是否符合期望。
  5. 处理多项输入时使用正确的节点类型:处理多个输入项目时,Code节点通常比Function节点更适合。

最终架构

优化后的数据流架构如下:

LinkedIn抓取 → 存储到Supabase → 传递ID引用 → AI分析 → 发送带ID的邮件 → 用户点击 → Webhook请求 → 从数据库检索完整信息 → 生成Cover Letter → 返回HTML页面

与之前的架构相比:

LinkedIn抓取 → 合并数据 → AI分析 → [丢失详细信息] → 发送邮件 → 用户点击 → 尝试重新抓取

新架构彻底解决了数据丢失问题,同时提供了更好的用户体验和系统可靠性。

数据存储与处理分离解决方案.svg

结语

这次优化过程让我深刻理解了自动化工作流中数据流设计的重要性。一个看似简单的问题——“数据丢失”——实际上揭示了更深层次的架构设计问题。通过重新思考数据流模式,我们不仅解决了当前问题,还为系统未来扩展奠定了更坚实的基础。

希望这篇文章能为正在构建自动化工作流的开发者提供有价值的参考,尤其是在处理复杂数据流时避免类似的陷阱。记住:优秀的数据流设计应该保留必要的信息,直到系统确实不再需要它们