1. 从一句指令到完整应用:积木智能体开发Todo的底层逻辑
1.1 为什么“先问两个问题”比直接写代码更值得关注
把“帮我开发一个Todo应用”这句话丢给一个智能体,它没有立刻开始生成代码,而是先反问了我两个问题。这个细节看起来不起眼,但它恰恰是区分“代码生成器”和“开发智能体”的分水岭。代码生成器的工作模式是:你给指令,它给文本,至于这段文本能不能跑、跑起来是不是你想要的,它不负责。而一个真正具备工程思维的智能体,在动手之前会先做需求澄清——这跟人类资深开发者的习惯完全一致。
我让积木智能体从零开发一个Todo应用,整个过程最有价值的不是最后那几百行代码,而是它在动手前问的那两个问题。这两个问题暴露了智能体对“开发”这件事的理解深度:它知道一个Todo应用看似简单,但“简单”背后藏着大量需要确认的决策点。比如数据存哪里、要不要多用户、交互形态是命令行还是图形界面、需不需要持久化。这些问题如果不问清楚,写出来的代码大概率要返工。
这篇文章适合几类人看:一是正在尝试用智能体做实际项目开发的人,想知道怎么跟智能体协作效率最高;二是对Todo应用开发感兴趣但不知道从何下手的新手,可以把它当作一个完整的项目拆解案例;三是对智能体产品设计有兴趣的人,可以从中看到“需求澄清”这个环节在产品层面的价值。我会把整个开发过程拆开,讲清楚每一步背后的考量,包括智能体为什么这么问、我怎么回答、代码怎么落地、中间踩了哪些坑。
1.2 积木智能体的工作模式与普通代码生成的区别
普通代码生成工具的工作流是线性的:输入描述,输出代码,结束。它不会主动跟你确认任何东西,你描述得模糊,它就按自己的默认理解去写,写出来不对你再改,改完再生成,来回好几轮。这种模式在写一个小函数、一个工具类的时候还能接受,但一旦涉及一个完整应用,问题就大了——因为应用级别的开发涉及架构决策,而架构决策一旦做错,后面改起来成本极高。
积木智能体的工作模式是带反馈环的。它在接收到“开发一个Todo应用”这个指令后,先做了一轮内部推理,识别出这个需求中存在多个歧义点,然后选择把最关键的两个歧义点抛回给我。这个行为在工程上叫“需求澄清”,在敏捷开发里叫“故事梳理”,在智能体领域叫“交互式任务分解”。不管叫什么,核心逻辑是一样的:在信息不完整的情况下,先补齐关键信息再动手,比闷头写完再返工要高效得多。
我实测下来,这种“先问后做”的模式在开发类任务中优势非常明显。第一,它避免了大量无效生成。你想想,如果智能体默认用localStorage做存储,但你实际想要的是后端数据库,那它生成的整个数据层代码全部作废。第二,它让最终产物更贴近真实需求。因为你在回答问题的过程中,自己也会把需求想得更清楚。第三,它降低了后续修改的沟通成本。代码是双方确认过需求之后生成的,改起来方向明确。
1.3 两个问题背后的需求拆解框架
积木智能体问我的两个问题,我后来复盘了一下,发现它们不是随便问的,而是遵循了一个清晰的需求拆解框架。
第一个问题是关于数据持久化方式的:数据是存在浏览器本地,还是需要一个后端服务来存储?这个问题直接决定了整个应用的技术架构。如果选本地存储,那就是纯前端方案,一个HTML文件加一段JavaScript就能跑,部署成本为零。如果选后端存储,那就需要设计API接口、选数据库、考虑跨设备同步,工作量至少翻三倍。
第二个问题是关于交互形态的:你希望是一个命令行工具,还是一个有图形界面的网页应用?这个问题决定了用户界面的技术选型和交互设计方向。命令行工具用Node.js写,输出用console.log,交互靠readline模块。网页应用则需要HTML+CSS+JavaScript三件套,还要考虑响应式布局和移动端适配。
这两个问题覆盖了应用开发中最核心的两个维度:数据层和表现层。数据层决定信息怎么存、怎么取,表现层决定用户怎么用、怎么交互。把这两个维度确认清楚,剩下的就是按部就班的实现了。这个拆解框架其实可以套用到任何CRUD类应用的开发中,不管是Todo、笔记、记账还是通讯录,核心决策点都是这两个。
2. 需求确认与技术选型:两个问题如何决定项目走向
2.1 第一个问题:数据存哪里——本地存储与后端存储的取舍
积木智能体问的第一个问题原文大概是:“这个Todo应用的数据你希望存在浏览器本地,还是需要一个后端服务来持久化?”这个问题看起来简单,但它背后涉及的技术决策链条很长。
如果选本地存储,具体方案又有几种:localStorage、sessionStorage、IndexedDB。localStorage最简单,键值对存储,容量大概5MB,同步读写,适合小规模数据。sessionStorage跟localStorage用法一样,但关掉标签页数据就没了,不适合Todo这种需要持久化的场景。IndexedDB是浏览器内置的NoSQL数据库,容量大、支持索引和事务,但API比较复杂,写起来代码量大。对于一个Todo应用来说,localStorage完全够用,除非你要存图片或者大量附件。
如果选后端存储,那要考虑的东西就多了:用什么语言写后端(Node.js、Python、Go)、用什么数据库(SQLite、MySQL、MongoDB)、API怎么设计(RESTful还是GraphQL)、要不要做用户认证、部署在哪里。这一套下来,项目复杂度直接从“一个下午能搞定”变成“至少一周的工作量”。
我当时的回答是选本地存储。理由很直接:这是一个个人使用的Todo工具,不需要跨设备同步,不需要多人协作,数据量很小,localStorage的性能和容量完全够用。而且本地存储意味着零部署成本,打开浏览器就能用,不需要维护服务器。这个决策背后的逻辑是:用最简单的方案满足当前需求,不为不存在的需求提前买单。
注意:localStorage有一个容易被忽略的限制——它只能存字符串。存对象的时候需要先用JSON.stringify序列化,取出来的时候用JSON.parse反序列化。这个转换过程如果不做,存进去的就是[object Object],取出来没法用。
2.2 第二个问题:交互形态——命令行与图形界面的适用场景
第二个问题是:“你希望它是一个命令行工具,还是一个有图形界面的网页应用?”这个问题决定了用户跟应用交互的方式,也决定了技术栈的选择。
命令行工具的优点是开发快、依赖少、适合自动化脚本调用。用Node.js写一个CLI工具,核心代码可能就一两百行,用readline模块处理输入,用console.log输出结果,用process.argv接收参数。缺点是交互体验差,用户需要记住命令格式,没有可视化反馈,在手机上基本没法用。
图形界面的优点是直观、易用、跨平台。一个网页版的Todo应用,打开浏览器就能用,支持鼠标点击、拖拽排序、键盘快捷键,还能做响应式适配手机屏幕。缺点是开发工作量更大,需要写HTML结构、CSS样式、JavaScript交互逻辑,还要处理各种浏览器兼容性问题。
我选了网页应用。原因是我自己用Todo工具的场景主要是在电脑浏览器里,偶尔在手机上记一下。网页应用可以同时覆盖这两个场景,而且可视化界面对于任务管理来说体验更好——你能一眼看到所有任务的状态,拖拽排序也比输入命令直观得多。
这个决策背后的逻辑是:交互形态的选择要匹配使用场景。如果你是要做一个被其他程序调用的工具,CLI更合适;如果是给人用的日常工具,GUI更合适。Todo应用显然是给人用的,所以选网页应用。
2.3 技术栈确定:为什么选原生三件套而不是框架
需求确认之后,技术栈的选择就顺理成章了。纯前端网页应用,数据存localStorage,那技术栈就是HTML+CSS+JavaScript。有人可能会问:为什么不用React、Vue或者Svelte?这些框架不是开发效率更高吗?
我的考虑是这样的:对于一个Todo应用来说,框架带来的收益很小,但引入的成本不低。收益小是因为Todo应用的交互逻辑很简单——增删改查加一个状态切换,没有复杂的组件通信和状态管理需求。成本不低是因为用框架就需要构建工具、需要理解框架的生命周期和响应式原理、需要处理依赖版本问题。对于一个几百行代码就能搞定的项目,引入框架反而是过度工程。
原生三件套的优势在于:零依赖、零构建、打开HTML文件就能跑、代码量可控、调试直观。你不需要装Node.js,不需要跑npm install,不需要配webpack或者vite。一个index.html文件,里面内联CSS和JavaScript,双击就能在浏览器里打开使用。这种极简的交付方式对于个人工具来说非常合适。
当然,如果这个Todo应用后续要扩展成多用户协作、要加实时同步、要做复杂的筛选和标签系统,那确实应该考虑框架。但在当前需求下,原生方案是最优解。这个决策逻辑可以总结为一句话:技术选型要匹配项目复杂度,不要用大炮打蚊子。
3. 从零到一:Todo应用的核心功能实现
3.1 数据模型设计:一个任务对象应该包含哪些字段
在动手写代码之前,先要把数据模型定下来。一个Todo任务对象应该包含哪些字段?我列了一下:
- id:唯一标识符,用于区分不同任务。用时间戳加随机数生成,保证不重复。
- text:任务内容,用户输入的文本。
- completed:完成状态,布尔值,true表示已完成,false表示未完成。
- createdAt:创建时间,用于排序和展示。
- updatedAt:最后修改时间,用于追踪任务变更。
这五个字段覆盖了Todo应用的核心需求。id用于操作定位,text是任务本体,completed是状态标记,createdAt和updatedAt是辅助信息。有些Todo应用还会加优先级、标签、截止日期、备注等字段,但那些属于扩展功能,第一版不做。
数据模型定好之后,存储结构就清晰了:一个任务数组,每个元素是一个任务对象,整个数组序列化成JSON字符串存到localStorage里。读取的时候反序列化回来,就是一个可以直接操作的对象数组。
// 任务对象的典型结构 { id: 1700000000000, text: "完成Todo应用开发", completed: false, createdAt: "2024-01-15T10:30:00.000Z", updatedAt: "2024-01-15T10:30:00.000Z" }提示:id用Date.now()生成在极端情况下可能重复——如果同一毫秒内创建了两个任务。更稳妥的做法是Date.now()加上一个随机数后缀,或者用一个自增计数器配合时间戳。
3.2 核心功能拆解:增删改查与状态切换
Todo应用的核心功能就五个:添加任务、删除任务、编辑任务、切换完成状态、展示任务列表。这五个功能对应五个操作函数,每个函数的逻辑都不复杂,但有几个细节需要注意。
添加任务:用户输入文本,点击添加按钮或者按回车,创建一个新任务对象,push到任务数组,然后保存到localStorage并重新渲染列表。这里要注意的是输入校验——空字符串不能添加,前后空格要trim掉,防止用户不小心输入一堆空格。
删除任务:用户点击删除按钮,根据任务id从数组中过滤掉对应元素,保存并重新渲染。删除操作最好加一个确认步骤,防止误删。可以用confirm弹窗,也可以做一个自定义的确认提示。
编辑任务:用户双击任务文本或者点击编辑按钮,把文本变成可编辑状态,修改完成后保存。编辑功能有两种实现方式:一种是原地编辑,文本区域变成input框;另一种是弹窗编辑,弹出一个对话框让用户修改。原地编辑体验更流畅,但实现起来稍微复杂一点。
切换完成状态:用户点击复选框或者任务文本,切换completed字段的值,保存并重新渲染。已完成的任务通常加删除线样式,视觉上跟未完成任务区分开。
展示任务列表:根据任务数组渲染DOM,每个任务对应一个列表项。列表项里包含复选框、任务文本、编辑按钮、删除按钮。渲染的时候要根据completed状态决定是否加删除线样式。
这五个功能写下来,核心JavaScript代码大概两百行左右。加上HTML结构和CSS样式,整个应用大概四百到五百行。这个体量对于一个功能完整的Todo应用来说是合理的,没有冗余代码,也没有为了炫技而增加的复杂度。
3.3 界面布局与交互细节:让工具用起来顺手
界面布局这块,我采用了经典的Todo应用布局:顶部是输入区域,中间是任务列表,底部是统计信息和操作按钮。
输入区域包含一个文本输入框和一个添加按钮。输入框的placeholder写“添加新任务...”,提示用户操作。添加按钮用主色调,视觉上突出。按回车也能触发添加,这是用户习惯的操作方式。
任务列表区域是核心。每个任务项左边是复选框,中间是任务文本,右边是编辑和删除按钮。复选框用自定义样式,比浏览器默认的复选框好看。任务文本在未完成状态下是正常颜色,完成后变成灰色加删除线。编辑和删除按钮默认半透明,鼠标悬停时变完全不透明,这样界面看起来更干净。
底部统计区域显示“还有X项未完成”,让用户对任务进度有个直观感知。旁边放一个“清除已完成”按钮,一键删除所有已完成任务。这个功能很实用,避免已完成任务堆积在列表里。
交互细节方面,有几个地方值得注意。第一,添加任务后输入框要自动清空并保持焦点,方便连续添加。第二,编辑任务时按Esc取消编辑,按Enter保存,这是符合直觉的快捷键设计。第三,删除任务时如果列表为空,显示一个空状态提示,而不是一片空白。第四,任务列表滚动时,输入区域固定在顶部,不随列表滚动。
这些细节看起来小,但直接影响使用体验。一个Todo工具好不好用,往往就体现在这些地方。功能大家都有,但用起来顺不顺手,差别就在细节里。
4. 实操过程全记录:从空白文件到可用应用
4.1 项目结构搭建与基础HTML骨架
整个项目就三个文件:index.html、style.css、app.js。没有构建工具,没有依赖管理,没有配置文件。这种极简结构的好处是清晰——打开文件夹就知道每个文件是干什么的。
index.html的结构很标准:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Todo</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="app"> <header class="app-header"> <h1>我的待办</h1> <div class="input-area"> <input type="text" id="taskInput" placeholder="添加新任务..."> <button id="addBtn">添加</button> </div> </header> <main class="task-list" id="taskList"></main> <footer class="app-footer"> <span id="taskCount">0 项未完成</span> <button id="clearCompleted">清除已完成</button> </footer> </div> <script src="app.js"></script> </body> </html>这个结构里,header放输入区域,main放任务列表,footer放统计和操作。语义化标签让结构更清晰,也方便后续用CSS选择器定位。
注意:viewport的meta标签一定要加,否则在手机上打开时页面会按桌面宽度渲染,字小得看不清。这个标签是移动端适配的基础,很多人写网页应用时会漏掉。
4.2 样式设计:用CSS变量管理配色与间距
CSS这块我用CSS变量来管理配色和间距,这样后续调整主题色的时候只需要改一个地方。
:root { --primary: #4a90d9; --primary-hover: #3a7bc8; --bg: #f5f6fa; --card-bg: #ffffff; --text: #2c3e50; --text-light: #95a5a6; --border: #e1e4e8; --danger: #e74c3c; --radius: 8px; --spacing: 16px; }配色方案走的是清爽路线:浅灰背景,白色卡片,蓝色主色调,灰色辅助文字。这种配色不刺眼,适合长时间使用。间距用统一的变量控制,保证界面元素之间的呼吸感一致。
任务项的样式有几个关键点:用flex布局让复选框、文本、按钮水平排列;文本区域用flex:1占满剩余空间;按钮默认opacity:0.5,hover时变成1,这样界面看起来更干净;已完成任务的文本加删除线和灰色,视觉上弱化。
响应式方面,在窄屏幕上(比如手机),把输入区域的按钮和输入框改成垂直排列,任务项的按钮稍微缩小一点。用媒体查询处理,断点设在600px。
4.3 JavaScript核心逻辑:状态管理与渲染函数
JavaScript这块是整个应用的核心。我采用了一个简单的状态管理模式:一个tasks数组存所有任务,一个render函数负责把数组渲染成DOM,所有操作都是先改数组、存localStorage、再调render。
let tasks = JSON.parse(localStorage.getItem('todos') || '[]'); function save() { localStorage.setItem('todos', JSON.stringify(tasks)); } function render() { const list = document.getElementById('taskList'); if (tasks.length === 0) { list.innerHTML = '<div class="empty">暂无任务,添加一个吧</div>'; } else { list.innerHTML = tasks.map(task => ` <div class="task-item ${task.completed ? 'completed' : ''}">document.getElementById('taskList').addEventListener('click', (e) => { const taskItem = e.target.closest('.task-item'); if (!taskItem) return; const id = Number(taskItem.dataset.id); if (e.target.type === 'checkbox') { toggleTask(id); } else if (e.target.classList.contains('delete-btn')) { deleteTask(id); } else if (e.target.classList.contains('edit-btn')) { editTask(id); } });4.4 数据持久化与边界情况处理
localStorage的读写虽然简单,但有几个边界情况需要处理。
第一,首次使用时localStorage里没有数据,localStorage.getItem('todos')返回null,JSON.parse(null)会报错。所以要用|| '[]'做兜底,保证首次加载时tasks是空数组。
第二,localStorage的数据可能被用户手动修改或者被其他脚本污染,导致JSON.parse失败。稳妥的做法是用try-catch包住解析过程,解析失败时回退到空数组。
function loadTasks() { try { return JSON.parse(localStorage.getItem('todos')) || []; } catch (e) { console.warn('数据解析失败,使用空列表'); return []; } }第三,localStorage有容量限制,大概5MB。对于纯文本的Todo任务来说,这个容量能存几万条任务,正常使用完全够用。但如果后续要加图片附件,就需要换IndexedDB或者后端存储。
第四,多个标签页同时打开时,一个标签页的修改不会自动同步到另一个标签页。可以用storage事件监听localStorage的变化,实现跨标签页同步。不过对于个人Todo工具来说,这个需求不强烈,第一版可以不做。
5. 常见问题与排查技巧实录
5.1 数据不保存或刷新后丢失的排查思路
这是Todo应用最常见的问题:添加了任务,刷新页面后任务没了。排查思路按以下顺序来:
第一步,打开浏览器开发者工具,切到Application面板,看Local Storage里有没有todos这个键。如果没有,说明save函数没被调用,或者调用时出错了。检查添加任务的函数里有没有调save。
第二步,如果有todos键但值是空数组,说明save被调用了但tasks数组是空的。检查添加任务时有没有正确push到tasks数组。
第三步,如果todos键有数据但页面没显示,说明render函数没被调用或者渲染逻辑有问题。检查添加任务后有没有调render。
第四步,如果以上都正常但刷新后还是丢失,检查是不是用了sessionStorage而不是localStorage。sessionStorage在标签页关闭后数据就没了。
实操心得:我习惯在save和render函数里各加一行console.log,开发阶段能快速定位问题出在哪个环节。上线前再把日志去掉。
5.2 事件绑定失效与动态元素处理
动态生成的DOM元素,如果直接给它们绑定事件,重新渲染后事件会丢失。比如你给每个删除按钮绑定了click事件,但列表重新渲染后,新的删除按钮没有绑定事件,点击就没反应。
解决方案有两种:一是每次渲染后重新绑定所有事件,二是用事件委托,把事件绑在父元素上,通过事件冒泡捕获子元素的点击。我推荐事件委托,代码更简洁,也不会因为重新渲染而失效。
事件委托的关键是用e.target.closest('.task-item')找到被点击的任务项,然后从dataset.id里取出任务id。这样不管列表怎么重新渲染,事件始终有效。
5.3 编辑功能的状态管理与焦点控制
编辑功能有一个容易忽略的细节:进入编辑状态后,输入框要自动获得焦点,并且光标要定位到文本末尾。如果不做这个处理,用户双击任务后还要再点一下输入框才能输入,体验很差。
实现方式是编辑状态下把任务文本替换成input元素,然后调input.focus(),再用input.setSelectionRange(len, len)把光标移到末尾。保存编辑时,按Enter触发保存,按Esc取消编辑恢复原文本。
还有一个细节:编辑状态下如果用户点击了页面其他地方,应该自动保存还是取消?我选择自动保存,因为用户点击其他地方通常意味着编辑完成了。但要在blur事件里加一个短暂的延迟,否则点击保存按钮时blur先触发,会导致保存两次。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 刷新后数据丢失 | 未调用save或用了sessionStorage | 检查Application面板 | 确保用localStorage并调用save |
| 删除按钮点击无反应 | 事件绑定在动态元素上 | 检查事件绑定方式 | 改用事件委托 |
| 编辑时无法输入 | 输入框未获得焦点 | 检查是否调用了focus | 编辑后调input.focus() |
| 任务文本显示为HTML | 未做转义 | 输入<b>test</b>测试 | 添加escapeHtml函数 |
| 移动端显示异常 | 缺少viewport标签 | 检查head区域 | 添加viewport meta标签 |
| 多个标签页不同步 | localStorage无跨页通知 | 开两个标签页测试 | 监听storage事件 |
6. 智能体协作开发的几点体会
6.1 需求澄清环节的价值被严重低估
这次用积木智能体开发Todo应用,最大的感受是:需求澄清环节的价值被严重低估了。大多数人用智能体开发时,习惯一次性把需求描述得很详细,恨不得把所有细节都写进去。但实际上,把需求描述清楚这件事本身就很难,你很难在动手之前想到所有需要确认的点。
智能体主动提问的好处是,它帮你把需要确认的点列出来了。你只需要回答,不需要自己从头梳理。这就像有一个经验丰富的搭档,在你动手之前提醒你:“等一下,这几个地方你确认了吗?”这种协作模式比单方面输出指令要高效得多。
我后来想了一下,如果智能体没问那两个问题,我会怎么描述需求?我大概率会说“帮我写一个Todo应用,用HTML+CSS+JavaScript,数据存localStorage”。这个描述看起来完整,但其实跳过了“为什么选localStorage”这个决策过程。智能体问出来之后,我被迫思考了一下,确认localStorage确实是最优解。这个思考过程本身就有价值。
6.2 从“写代码”到“做产品”的思维转变
智能体问的那两个问题,本质上是在引导我从“写代码”的思维切换到“做产品”的思维。写代码的思维是:给我一个功能描述,我把它翻译成代码。做产品的思维是:先想清楚用户是谁、使用场景是什么、核心需求是什么,然后再决定技术方案。
数据存哪里、交互形态是什么,这两个问题都是产品层面的决策,不是技术层面的决策。技术层面只关心“怎么实现”,产品层面才关心“实现什么”和“为什么这么实现”。智能体把这两个问题前置,实际上是在帮我做产品定义。
这个思维转变对于开发者来说很重要。很多人技术能力很强,但做出来的东西不好用,就是因为缺少产品思维。智能体通过提问的方式,把产品思维注入到开发流程里,这是一个很聪明的设计。
6.3 后续扩展方向与迭代思路
这个Todo应用目前是单机版,数据存在浏览器本地。后续如果要扩展,有几个方向:
多设备同步:把数据从localStorage迁移到后端数据库,加一个简单的用户认证,就能实现跨设备同步。后端可以用Node.js+SQLite,部署成本很低。
任务分类与标签:给任务加分类字段和标签字段,支持按分类筛选、按标签搜索。数据模型扩展一下,界面上加筛选栏。
截止日期与提醒:给任务加dueDate字段,界面上用日期选择器设置,到期前用浏览器通知提醒。Notification API可以做到。
数据导入导出:加一个导出按钮,把任务数据导出成JSON文件;加一个导入按钮,从JSON文件恢复数据。这个功能对于数据备份和迁移很实用。
键盘快捷键:加一组键盘快捷键,比如/聚焦输入框、Esc取消编辑、Ctrl+Enter保存。对于重度用户来说,键盘操作比鼠标快得多。
这些扩展方向不需要一次性全做,可以按需迭代。关键是第一版要把核心功能做扎实,数据模型设计得可扩展,后续加功能才不会推倒重来。
6.4 智能体开发工具的选型建议
市面上的智能体开发工具不少,选型的时候我建议关注几个点:
第一,看它会不会主动提问。只会被动执行指令的工具,适合做明确的小任务;会主动澄清需求的工具,适合做完整的项目开发。
第二,看它生成的代码质量。有些工具生成的代码能跑但结构混乱,后续没法维护。好的工具生成的代码应该结构清晰、命名规范、有适当的注释。
第三,看它能不能理解上下文。开发过程中你会不断修改需求,工具要能记住之前的决策,不能每次修改都从头开始。
第四,看它的调试支持。出问题的时候能不能帮你定位,能不能解释代码的逻辑,这比单纯生成代码更重要。
积木智能体在这几个方面表现都不错,尤其是主动提问这一点,在实际开发中帮助很大。当然,工具只是辅助,核心还是你自己的开发思维和工程能力。智能体能帮你写代码,但帮不了你做决策。决策还是得自己来。