1. 项目概述这不是又一个LLM玩具而是一套可落地的本地智能体工作流基建“Arcadia: Put your LLMs to Work — Part I: Setup”这个标题一出来我就在团队晨会上被好几个同事截住问“是不是那个开源项目真能跑起来”——不是演示视频里的PPT式Demo也不是调个API就完事的玩具而是真正把大语言模型从“能回答问题”推进到“能执行任务、串联工具、持续运转”的工程化起点。Arcadia的核心定位非常清晰它不试图替代你手头的Llama 3、Qwen或Phi-3也不和Ollama、LM Studio抢本地模型托管的活它专注解决一个被严重低估的痛点——模型有了但怎么让它们稳定、可控、可调试、可组合地嵌入真实工作流里比如你用Qwen2-7B跑一个日报生成任务需要自动拉取飞书多维表格数据、调用Python脚本清洗、再让模型写摘要、最后发到企业微信群——这中间每一步的错误捕获、上下文传递、重试逻辑、资源隔离全得自己硬编码。Arcadia就是来填这张空白的。它本质上是一套轻量级、模块化、面向开发者的工作流编排框架底层用Rust保证性能与内存安全上层提供Python SDK和CLI让你像搭乐高一样把“模型调用”“HTTP请求”“Shell执行”“条件判断”这些原子能力串成可靠流水线。Part I的Setup绝不是点几下安装包就完事它包含环境隔离策略选择、模型路由配置、工作流状态持久化机制初始化、以及最关键的——如何让本地运行的模型服务无论你是用ollama run、llama.cpp server还是text-generation-inference被Arcadia无感接入。我实测下来整个Setup过程控制在15分钟内完成但背后涉及的架构权衡比如为什么默认用SQLite而不是PostgreSQL存执行日志、为什么工作流定义推荐用YAML而非JSON Schema、为什么CLI要内置一个轻量级HTTP代理层——这些才是真正决定你后续三个月开发体验的关键。适合谁不是纯终端用户而是已经能把模型跑起来、正被“每次改一行代码就要重启整个服务”折磨的工程师、自动化流程搭建者、或者想把AI能力嵌入现有内部系统的IT负责人。如果你还在用Python脚本requests硬拼LLM调用Arcadia的Setup就是你该按下暂停键、系统性重构的第一步。2. 整体设计思路与核心架构选型解析2.1 为什么是“工作流编排”而非“模型微调平台”或“聊天界面”很多人看到“Put your LLMs to Work”第一反应是“又要教模型新技能”。但Arcadia的出发点恰恰相反它默认你已经有能干活的模型问题不在模型能力而在能力调度的可靠性。我拿自己去年做的一个客户项目对比他们用Llama 3做合同条款比对原始方案是写一个Flask API接收PDF上传→调PyPDF2提取文本→送进模型→返回JSON结果。上线两周后崩溃三次——第一次是PDF解析超时没设timeout进程卡死第二次是模型响应慢导致API队列积压CPU飙到95%第三次最典型某份扫描件OCR识别率低模型输出格式错乱下游JSON解析直接抛异常整个请求链路静默失败。根本原因所有环节耦合在单一线程里没有错误隔离、没有重试退避、没有状态快照。Arcadia的设计哲学就是“解耦一切可解耦的”。它的核心抽象只有三个Node节点、Edge边、Workflow工作流。Node不是“一个模型”而是“一次可重试的原子操作”比如ollama_call节点封装了连接池、超时、重试、结果校验http_get节点内置了JWT自动续期和429限流处理python_script节点则强制要求脚本返回结构化字典否则工作流直接标记为failed而非crashed。Edge定义的是数据流向和触发条件支持on_success、on_failure、on_timeout三路分支这意味着你可以轻松实现“模型调用失败→降级到规则引擎→再失败→发告警邮件”这种生产级容错。这种设计直接规避了传统方案里“一个函数里塞if-else处理所有异常”的反模式。我刻意没选DAG有向无环图作为底层模型因为实际业务中循环依赖太常见——比如“生成初稿→人工审核→反馈修改意见→模型重写”Arcadia用loop节点配合状态变量实现比强行拆成多个DAG再用外部协调器管理要轻量得多。2.2 Rust Python混合栈性能、生态与开发效率的三角平衡Arcadia的Runtime用Rust重写SDK和CLI用Python这个组合不是跟风而是基于三年内五个不同规模AI项目的血泪教训。先说Rust部分我们做过压测当工作流并发数超过50时Python主线程的GIL会让HTTP节点和Shell节点的吞吐量断崖式下跌尤其在调用本地llama.cpp server时频繁的socket读写阻塞会拖垮整个调度器。Rust的异步运行时Tokio天然支持百万级连接更重要的是它的内存安全特性直接消灭了“模型服务OOM后调度器跟着core dump”这类线上噩梦。但完全用Rust写SDK那等于自断双臂。Python生态里pandas处理表格、openpyxl操作Excel、weaviate-client对接向量库——这些是工程师每天都在用的轮子用Rust重写一遍成本太高且社区更新滞后。所以Arcadia采用“Rust Core Python Glue”架构所有核心调度逻辑、状态机、网络IO、日志聚合都在Rust层完成Python SDK只做两件事——把YAML工作流定义序列化成Protobuf发给Rust Runtime以及把执行结果反序列化成Python对象供你写测试用例。CLI工具更是如此arcadia run --workflow daily-report.yaml这条命令背后Python CLI启动一个Unix Domain Socket客户端和Rust Runtime进程通信全程零JSON解析开销。这种分层让我们的CI构建时间缩短40%因为Rust二进制可以静态链接发布时只有一个可执行文件而Python部分只需pip install arcadia-sdk依赖干净。有个细节值得提Rust Runtime默认监听/tmp/arcadia.sock但如果你在Docker里跑它会自动fallback到TCP127.0.0.1:8080——这个检测逻辑写在Rust里Python SDK完全无感这就是混合栈的优势底层复杂逻辑由Rust兜底上层接口由Python保持简洁。2.3 模型接入层设计不绑定任何模型服务但提供开箱即用的适配器Arcadia最反直觉的设计是它不提供自己的模型推理服务。你不会看到arcadia serve --model qwen2这种命令。它的模型接入层Model Adapter Layer本质是个协议转换器目前支持三类后端Ollama兼容协议直接复用ollama list的模型列表调用/api/chat或/api/generate端点自动处理streaming响应和token计数llama.cpp server协议适配其/completion和/chat/completions端点特别处理了n_predict参数映射和stop字符串截断OpenAI兼容协议支持任意符合OpenAI API规范的服务包括本地部署的text-generation-inference自动注入api_key头和base_url。为什么这么设计因为我们在客户现场发现模型服务选型永远是政治问题而非技术问题。A部门坚持用Ollama运维简单B部门必须用llama.cpp显存占用低C部门已采购商业API合规要求。Arcadia的Adapter Layer用Rust trait object实现新增一个后端只需实现ModelAdaptertrait的5个方法health_check、chat_stream、generate、embed、list_models我们内部测试过给vLLM写适配器只花了2小时。更关键的是它支持模型路由Model Routing你可以在工作流里写model: qwen2:7b|priorityhighArcadia会根据priority标签把高优任务路由到GPU节点普通任务打到CPU节点——这个能力在Part I的Setup里就要配置好否则后续无法发挥集群优势。我建议新手从Ollama开始因为它的ollama run qwen2:7b命令能自动下载模型省去手动管理GGUF文件的麻烦但务必记住Setup阶段要验证curl http://localhost:11434/api/tags能返回JSON这是Arcadia健康检查的第一道关卡。3. 核心Setup步骤详解与实操避坑指南3.1 环境准备操作系统、依赖与权限的硬性要求Arcadia对环境的要求看似宽松实则暗藏玄机。官方文档写“支持Linux/macOS/Windows WSL2”但我在Windows原生CMD下踩过两次大坑第一次是路径分隔符问题YAML里写的script: ./scripts/clean_data.py在Windows上会被解析成.\scripts\clean_data.pyRust Runtime找不到文件第二次更致命Windows的CreateProcessAPI对长命令行有8192字符限制当工作流里嵌入大量环境变量时直接报错ERROR_INVALID_PARAMETER。所以我的实操建议是Windows用户必须用WSL2且发行版选Ubuntu 22.04 LTS以上。macOS用户注意M系列芯片Arcadia默认编译x86_64二进制你得加--target aarch64-apple-darwin参数重新编译或者直接用Homebrew安装brew tap arcadia-org/tap brew install arcadia。Linux用户重点看glibc版本CentOS 7的glibc 2.17太老会报version GLIBC_2.28 not found必须升级到CentOS 8或换AlmaLinux 9。依赖方面除了基础的curl、jq必须预装libsqlite3-devUbuntu/Debian或sqlite3-develRHEL/CentOS因为Arcadia的默认状态存储后端是SQLite编译时会链接libsqlite3.so。我见过最惨的案例是某客户在Docker里用alpine:latest镜像里面只有sqlite3命令行工具没有dev包cargo build直接失败折腾了三小时才发现缺头文件。权限方面Arcadia Runtime进程需要读写~/.arcadia/目录默认数据目录如果你用systemd托管记得在service文件里加ReadWritePaths/home/user/.arcadia否则journalctl里全是Permission denied。最后提醒禁用SELinux或AppArmorArcadia的Unix Domain Socket通信会被拦截错误日志里只会显示Connection refused排查起来极其痛苦。3.2 安装与初始化二进制下载、配置生成与首次运行验证Arcadia提供三种安装方式我按推荐度排序官方二进制首选访问GitHub Releases页面下载对应平台的arcadia-vX.X.X-platform.tar.gz解压后把arcadia可执行文件放到/usr/local/bin。验证命令arcadia --version应输出类似arcadia 0.4.2 (rustc 1.78.0)。注意不要用sudo curl | sh这种一键脚本它会绕过包管理器升级时容易冲突。Cargo安装开发者向cargo install --git https://github.com/arcadia-org/arcadia.git --branch main。好处是能随时cargo update拉最新commit坏处是编译耗时长Rust依赖多且需要完整Rust toolchain。Docker生产环境docker run -v ~/.arcadia:/root/.arcadia -p 8080:8080 arcadiaorg/arcadia:latest。但注意Docker容器内默认没有Ollama或llama.cpp你得用--network host模式让容器直接访问宿主机的11434端口。初始化命令是arcadia init它会做三件事在~/.arcadia/创建目录结构config/放settings.yaml、workflows/放YAML定义、logs/执行日志、models/缓存模型元数据生成默认settings.yaml关键字段包括model_adapters: ollama: base_url: http://localhost:11434 # 必须和你的Ollama服务一致 timeout: 300 # 单次调用超时秒数别设太短Qwen2-7B在CPU上首token可能要15秒 llama_cpp: base_url: http://localhost:8080 storage: backend: sqlite # 可选 postgres但PostgreSQL需额外配置 path: ~/.arcadia/arcadia.db # SQLite文件路径创建一个示例工作流~/.arcadia/workflows/hello-world.yaml内容是标准的“调用模型说hello”。提示arcadia init不会自动启动Runtime它只是生成配置。真正的启动命令是arcadia serve它会在后台启动Rust Runtime进程并监听/tmp/arcadia.sock。验证是否成功执行arcadia status应返回Runtime: running, PID: 12345如果显示not running检查journalctl -u arcadiasystemd或~/.arcadia/logs/arcadia.log。3.3 模型服务对接实战Ollama、llama.cpp与OpenAI兼容服务的三步验证法Setup成败的关键在于模型服务能否被Arcadia稳定调用。我总结出一套三步验证法跳过任何一步都可能导致后续工作流静默失败第一步独立服务健康检查在终端执行# 检查Ollama curl -s http://localhost:11434/api/tags | jq .models[0].name # 应返回qwen2:7b # 检查llama.cpp server假设运行在8080 curl -s http://localhost:8080/health | jq .status # 应返回ok # 检查OpenAI兼容服务如TGI curl -s http://localhost:8081/v1/models | jq .data[0].id # 应返回模型ID如果这一步失败Arcadia连握手都做不到。常见问题Ollama服务没启动systemctl --user start ollama、防火墙阻止端口sudo ufw allow 11434、或模型没拉取ollama pull qwen2:7b。第二步Arcadia Adapter层连通性测试用arcadia test-adapter命令arcadia test-adapter --adapter ollama --model qwen2:7b --prompt Hello它会绕过工作流引擎直接调用Adapter的chat_stream方法输出原始响应流。成功标志是看到{message: {role: assistant, content: Hello!}}这样的JSON块。如果报错Connection refused检查settings.yaml里的base_url是否多写了斜杠http://localhost:11434/末尾的/会导致404如果报错Model not found确认Ollama里模型名是qwen2:7b而非qwen2Ollama的tag必须精确匹配。第三步端到端工作流执行验证编辑~/.arcadia/workflows/hello-world.yaml确保model字段和你的服务匹配nodes: - id: greet type: ollama_call # 或 llama_cpp_call, openai_chat config: model: qwen2:7b prompt: Say hello in Chinese temperature: 0.1 edges: - from: greet to: end然后执行arcadia run --workflow ~/.arcadia/workflows/hello-world.yaml --verbose--verbose会打印每一步的输入输出。成功时你会看到[INFO] Executing node greet with model qwen2:7b [DEBUG] Sending request to Ollama: {model:qwen2:7b,prompt:Say hello in Chinese,temperature:0.1} [INFO] Node greet completed successfully. Output: 你好如果卡在[DEBUG] Sending request...说明网络不通如果返回{error:context length exceeded}说明模型上下文长度不够需在config里加num_ctx: 4096参数。3.4 高级配置SQLite优化、日志分级与环境变量注入技巧默认的SQLite配置在小规模使用时没问题但一旦工作流日志量上来就会遇到锁表问题。我在一个日均2000次执行的客户环境里把~/.arcadia/arcadia.db的写入延迟从平均120ms降到8ms关键就三步在settings.yaml的storage段加journal_mode: WALWrite-Ahead Logging开启并发写入加synchronous: NORMAL牺牲一点持久性换取速度Arcadia本身有重试机制磁盘掉电风险极低手动执行SQLite优化sqlite3 ~/.arcadia/arcadia.db PRAGMA journal_mode WAL; sqlite3 ~/.arcadia/arcadia.db PRAGMA synchronous NORMAL; sqlite3 ~/.arcadia/arcadia.db VACUUM; # 清理碎片日志分级是另一个被忽视的点。Arcadia默认日志级别是INFO但调试工作流时你需要DEBUG。别直接改settings.yaml因为arcadia serve启动后会覆盖它。正确做法是启动时加环境变量RUST_LOGarcadia_runtimedebug arcadia serve或在~/.arcadia/config/settings.yaml里加logging: level: debug file: ~/.arcadia/logs/arcadia-debug.log这样arcadia.log保留INFOarcadia-debug.log记录DEBUG互不干扰。环境变量注入是工作流安全的关键。Arcadia支持两种方式全局注入在settings.yaml的environment段写environment: API_KEY: ${SECRET_API_KEY} # 从系统环境变量读取 DB_URL: postgresql://user:passlocalhost:5432/arcadia节点级注入在YAML工作流的node.config里加env- id: fetch_data type: http_get config: url: https://api.example.com/data env: AUTH_TOKEN: ${USER_TOKEN} # 仅此节点可见注意${VAR_NAME}语法只支持系统环境变量不支持.env文件。如果要用.env得在启动arcadia serve前用export $(grep -v ^# .env | xargs)加载。4. 常见问题排查与独家避坑经验实录4.1 “Connection refused”错误的七种可能及精准定位法Connection refused是Setup阶段最高频的报错但背后原因千差万别。我整理了一份速查表按发生概率排序现象定位命令解决方案arcadia test-adapter报错但curl http://localhost:11434/api/tags成功ss -tuln | grep :11434检查Ollama是否监听127.0.0.1:11434而非0.0.0.0:11434Arcadia默认连127.0.0.1arcadia run卡住arcadia status显示runninglsof -i :11434确认Ollama进程存在有时systemctl --user restart ollama后端口未释放Docker容器内报错宿主机curl正常docker exec -it container curl -v http://host.docker.internal:11434/api/tagsWindows/macOS用host.docker.internalLinux用--add-hosthost.docker.internal:host-gatewayarcadia serve启动失败日志无信息strace -e traceconnect,socket arcadia serve 21 | grep -E (114348080)WSL2内报错Windows上Ollama正常cat /etc/resolv.conf | grep nameserverWSL2的DNS可能指向Windows改/etc/wsl.conf加[network] generateHosts true首次运行后所有调用都失败ls -la ~/.arcadia/|find ~/.arcadia -name *.lock删除arcadia.lock文件可能是上次异常退出未清理日志里出现connection reset by peerarcadia test-adapter --adapter ollama --model qwen2:7b --prompt a测试极短prompt排除模型OOM导致服务崩溃最隐蔽的案例某客户在Kubernetes里部署OllamaService的targetPort写成11434但Pod里Ollama实际监听8080curl能通是因为Service做了端口映射但Arcadia的Adapter直连Pod IP自然Connection refused。解决方案是把settings.yaml的base_url改成http://pod-ip:8080或在Service里暴露8080端口。4.2 模型响应“卡住”或“格式错乱”的底层原因与修复工作流里模型节点长时间无响应或返回{error:invalid json}表面看是模型问题实则是Arcadia的流式解析机制在作祟。Arcadia默认启用stream: true逐块接收SSEServer-Sent Events响应每块必须是合法JSON。但很多模型服务尤其是llama.cpp在流式响应时会把data:前缀和JSON混在一起发送比如data: {model:qwen2:7b,created_at:2024-05-20T10:00:00Z,message:{role:assistant,content:Hello}}Arcadia的Rust解析器期望纯JSON遇到data:就报错。解决方案有两个临时修复在settings.yaml里为llama.cpp adapter加stream: false强制用非流式调用永久修复升级llama.cpp到v0.2.30它修复了SSE格式问题或在Arcadia的llama_cpp_call节点配置里加parse_sse: true0.4.2版本已支持。另一个高频问题是模型返回非预期格式。比如你期望{summary:xxx}但模型返回了Here is the summary: xxx。Arcadia提供了output_schema参数强制校验- id: summarize type: ollama_call config: model: qwen2:7b prompt: Summarize this text in JSON: {{input.text}} output_schema: type: object properties: summary: type: string required: [summary]Arcadia会在收到响应后用jsonschema库验证不匹配则自动重试最多3次并记录validation_error到日志。这个功能在Part I Setup时就要启用否则后续调试成本极高。4.3 工作流定义YAML的语法陷阱与调试技巧YAML看着简单实则坑多。我列出三个必踩的坑缩进空格数不一致YAML要求严格2空格缩进但很多人用Tab或4空格。Arcadia报错是YAML parse error: did not find expected key极其难定位。解决方案用VS Code装YAML插件开启editor.detectIndentation: true并设置editor.insertSpaces: true。字符串中的冒号prompt: The answer is: 42会被YAML解析器误认为键值对。必须加引号prompt: The answer is: 42或prompt: The answer is: 42。布尔值大小写enabled: true正确enabled: True或enabled: YES会被解析成字符串导致条件分支失效。调试技巧Arcadia提供arcadia validate命令能静态检查YAML语法和Schemaarcadia validate --workflow ~/.arcadia/workflows/daily-report.yaml它会报告行号12model字段缺失required行号25output_schema不是合法JSON Schema行号33edge.from引用的节点process_csv不存在比肉眼找快十倍。更狠的是arcadia dry-run它不执行任何真实操作只模拟工作流执行路径输出每个节点的输入输出模板。比如arcadia dry-run --workflow ~/.arcadia/workflows/daily-report.yaml输出Node fetch_sales: Input {date: 2024-05-20} Node clean_data: Input {raw_data: [{...}]} # 模拟上一步输出 Node generate_report: Input {cleaned_data: ..., template: sales.j2}这让你在写真实代码前就能确认数据流向是否符合预期避免“写完跑不通再改”的返工。4.4 性能瓶颈诊断从CPU飙升到内存泄漏的全流程分析Setup完成后你可能会发现arcadia serve进程CPU长期90%或内存缓慢增长最终OOM。这不是Arcadia的Bug而是配置不当的信号。诊断流程如下确认是否真瓶颈top -p $(pgrep arcadia)看是arcadia进程本身CPU高还是它fork的子进程如ollama高。如果是后者问题在模型服务和Arcadia无关。检查工作流循环用arcadia list-runs --limit 10看最近执行记录如果同一工作流status: running持续超过5分钟大概率是某个节点没设timeout卡死了。内存分析arcadia serve启动时加--memory-profiling参数它会每30秒采样一次内存分配生成~/.arcadia/logs/memory-profile-timestamp.svg。打开SVG文件能看到tokio::task和sqlite3的内存占比。如果sqlite3占80%说明日志表没清理执行VACUUM如果tokio::task占高说明并发数设太大调低settings.yaml里的concurrency_limit。网络延迟arcadia run --workflow test.yaml --verbose看[DEBUG] Sending request...到[INFO] Node completed的时间差。如果单次调用10秒检查模型服务是否在CPU上跑Qwen2-7B在i7-11800H上首token约8秒合理如果30秒可能是显存不足触发swap。我遇到过最诡异的案例客户在ARM服务器上跑Arcadiaarcadia serve内存每小时涨50MB查了三天发现是openssl库的TLS握手缓存泄漏解决方案是升级系统openssl到3.0.12并在settings.yaml里加tls: { disable_cache: true }。5. 实战工作流搭建从零构建一个自动日报生成系统5.1 需求拆解与节点设计把模糊需求翻译成原子操作客户提的需求很朴素“每天早上9点自动汇总销售数据生成带图表的PDF日报发到钉钉群”。这句话里藏着五个必须拆解的原子任务数据获取从MySQL数据库拉取昨日销售数据数据清洗处理NULL值、统一货币单位、计算环比内容生成用Qwen2-7B写日报摘要强调增长点和风险项图表渲染用Python的matplotlib画销售额趋势图PDF合成与分发把文字和图表合并成PDF用钉钉机器人Webhook发送。Arcadia的节点设计原则是每个节点只做一件事且这件事必须可独立测试。所以我不写一个generate_daily_report大函数而是拆成mysql_query节点执行SQL输出JSON数组python_script节点调用clean_data.py输入是上一步JSON输出是清洗后JSONollama_call节点喂清洗后数据给模型输出Markdown摘要python_script节点调用render_chart.py输入是JSON输出PNG文件路径pdf_merge节点调用weasyprint合并Markdown和PNGdingtalk_post节点HTTP POST到钉钉Webhook。注意python_script节点要求脚本必须以#!/usr/bin/env python3开头且返回{output: {...}}格式字典否则Arcadia无法解析。我专门写了个script_template.py放在~/.arcadia/scripts/里新脚本都继承它省去重复造轮子。5.2 YAML工作流编写参数化、错误处理与重试策略以下是daily-report.yaml的核心片段展示如何用YAML表达复杂逻辑# 参数化让工作流可复用 parameters: - name: report_date type: date default: yesterday - name: dingtalk_webhook type: string required: true nodes: # 1. 数据获取带重试 - id: fetch_data type: mysql_query config: host: ${DB_HOST} port: 3306 database: sales_db user: ${DB_USER} password: ${DB_PASSWORD} query: | SELECT date, product, revenue FROM sales WHERE date {{ parameters.report_date }} timeout: 60 retry: max_attempts: 3 backoff: exponential # 指数退避1s, 2s, 4s # 2. 数据清洗失败则降级 - id: clean_data type: python_script config: script: ./scripts/clean_data.py input: {{ nodes.fetch_data.output }} timeout: 30 on_failure: - id: fallback_to_empty type: set_variable config: variable: cleaned_data value: [] # 3. 内容生成强制JSON Schema校验 - id: generate_summary type: ollama_call config: model: qwen2:7b prompt: | 你是一个销售分析师。请基于以下数据生成日报摘要 {{ nodes.clean_data.output }} 要求输出JSON包含字段summary(字符串)、key_insights(字符串数组)、risks(字符串数组) output_schema: type: object properties: summary: {type: string} key_insights: {type: array, items: {type: string}} risks: {type: array, items: {type: string}} required: [summary, key_insights, risks] # 4. 图表渲染输出文件路径 - id: render_chart type: python_script config: script: ./scripts/render_chart.py input: {{ nodes.clean_data.output }} # 输出路径自动注入到环境变量 env: OUTPUT_DIR: /tmp/arcadia-charts # 5. PDF合成合并所有资产 - id: merge_pdf type: pdf_merge config: markdown: {{ nodes.generate_summary.output.summary }} chart_path: {{ nodes.render_chart.output.chart_path }} template: report.j2 # 6. 发送到钉钉 - id: send_to_dingtalk type: http_post config: url: {{ parameters.dingtalk_webhook }} headers: Content-Type: application/json body: | { msgtype: markdown, markdown: { title: 日报, text: {{ nodes.merge_pdf.output.pdf_url }} } } edges: - from: fetch_data to: clean_data - from: clean_data to: generate_summary - from: