Ethics - 系统应该允许什么
核心问题:
我们应该允许什么?不应该允许什么?
Dimension 1 定义世界。
Dimension 2 验证世界。
Dimension 3 约束世界。
软件工程里的伦理不是抽象说教,而是系统如何分配:
- 权力
- 数据
- 默认值
- 责任
- 机会
- 纠错路径
- 后果
核心句:
技术上能做,不代表系统应该允许。
1. 权限是在分配权力
权限不是普通配置。
它决定:
- 谁能看什么?
- 谁能改什么?
- 谁能删除什么?
- 谁能代表别人行动?
- 谁能绕过规则?
判断问题
- 这个主体是谁?人、服务、组织还是外部应用?
- 它完成任务所需的最小能力是什么?
- 权限绑定在哪个资源范围?
- 默认是否拒绝?
- 高风险操作是否有审计?
核心句:
默认拒绝,明确允许。
2. Admin 不是万能神
坏模式:
if (user.role === "admin") return true
更好的方向:
canViewUserProfile(adminId, accountId)
canRefundPayment(adminId, paymentId)
canExportUserData(adminId)
canImpersonateUser(adminId, accountId)
高风险能力应该拆开,并加上:
- 二次确认
- 审计日志
- 审批
- 时间限制
- 数据脱敏
核心句:
管理员不是一种身份,而是一组高风险能力的集合。
3. 隐私从不收集开始
隐私不只是防泄露。
更根本的是:
不该收集的数据,最好一开始就不要进入系统。
新增字段前问:
- 这个数据是否必要?
- 是否可以用更粗粒度数据代替?
- 是否可以临时使用,不持久化?
- 是否会进入日志、缓存、分析或第三方?
- 保留多久?
- 如何删除或匿名化?
核心句:
不必要的数据不是资产,是风险。
4. 日志不是隐私垃圾桶
坏日志:
logger.info("request", {
body: req.body,
headers: req.headers,
})
可能记录:
- password
- token
- cookie
- payment data
- PII
- 私密内容
好方向:
logger.info("payment_request_created", {
accountId,
paymentId,
amount,
currency,
requestId,
})
核心句:
日志应该记录排查所需证据,不记录不必要的私人信息。
5. 系统要有对抗性思维
不要只写 user story,也要写 abuse case。
作为攻击者,我想批量注册账号领取试用,以便免费访问付费课程。
作为恶意用户,我想重复调用退款接口,以便获得退款但保留访问权。
设计功能时问:
- 这个能力能否被批量调用?
- 客户端传来的哪些字段不可信?
- 重复请求是否安全?
- 是否需要限流?
- 是否需要幂等键?
- 被滥用后如何止损?
核心句:
系统不仅要服务善意用户,也要防止恶意使用。
6. 客户端输入不能直接信
坏代码:
await purchaseCourse({
accountId: req.body.accountId,
courseId: req.body.courseId,
price: req.body.price,
})
好方向:
const accountId = session.accountId
const course = await courses.findById(req.body.courseId)
const price = await pricing.currentPriceFor(course.id)
核心句:
客户端可以表达意图,服务器必须重新验证事实。
7. 指标就是价值判断
推荐、排序、风控、审核都不是中立的。
优化什么,就会塑造什么。
| 优化指标 | 可能偏向 |
|---|---|
| 点击率 | 标题党、刺激内容 |
| 观看时长 | 让用户停留更久的内容 |
| 完课率 | 短课程、容易完成的课程 |
| 收入 | 高价课程 |
设计指标时问:
- 它鼓励什么行为?
- 它会牺牲什么?
- 谁受益?
- 谁被边缘化?
- 有没有 guardrail metric?
核心句:
没有中立指标。
8. 自动化决定必须可解释、可申诉
高影响决定包括:
- 封号
- 拒绝提现
- 下架课程
- 拒绝认证
- 风控拦截
这些决定应该有:
- decision record
- reason code
- policy version
- audit trail
- appeal path
核心句:
自动化可以执行决定,但不能承担责任。
9. Reason Code 是伦理基础设施
不要只返回:
Rejected
要有稳定原因:
ACCOUNT_SUSPENDED
COURSE_ARCHIVED
PAYMENT_RISK_HIGH
REFUND_WINDOW_EXPIRED
Reason code 服务:
- 用户解释
- 客服处理
- 工程排查
- 监控聚合
- 审计复盘
- 申诉定位
核心句:
失败方式也应该被系统承认和命名。
10. 反操控设计
Dark patterns 是利用设计诱导用户做出不符合真实意愿的选择。
危险信号:
- 取消订阅很难
- 默认勾选营销邮件
- 隐藏真实价格
- 拒绝按钮羞辱用户
- 默认数据共享
- 用视觉强弱误导选择
判断问题:
- 用户是否清楚知道自己在选择什么?
- 拒绝、取消、退出是否容易?
- 默认值是否保护用户?
- 摩擦是在保护用户,还是阻碍用户?
核心句:
界面不是中立容器,界面会影响选择。
11. 安全默认值
默认配置应该保护用户和系统。
好默认:
default deny
default private
short-lived token
read-only API token
safe error response
redacted logs
share link expires
核心句:
安全不应该依赖记忆,应该依赖默认值。
12. 人类介入不是绕过系统
人工 override 必须被系统承认和记录。
type OverrideRecord = {
decisionId: string
actorId: string
action: "approve_appeal" | "reject_appeal" | "manual_override"
reason: string
occurredAt: Date
}
核心句:
人类介入不是绕过系统,而是系统承认并记录的一种路径。
13. 后台是权力集中区
坏后台:
搜索用户 -> 查看所有信息 -> 任意修改 -> 无审计
好后台:
搜索用户 -> 默认脱敏 -> 查看敏感信息需理由 -> 所有操作审计 -> 高风险操作二次确认
核心句:
不要把后台当成没有伦理边界的地方。
14. 伦理要落到工程构件
| 原则 | 工程构件 |
|---|---|
| 最小权限 | policy, scoped role, deny by default |
| 隐私最小化 | PII classification, retention job |
| 可解释性 | reason code, decision record |
| 责任归属 | audit trail, actorId, policyVersion |
| 申诉纠错 | appeal workflow, reviewer tooling |
| 反操控 | UX review, guardrail metrics |
| 安全默认值 | default private, short-lived token |
| 滥用防护 | rate limit, idempotency |
核心句:
如果原则找不到工程落点,它很可能只是口号。
15. 伦理债
系统也会积累伦理债:
- 没有删除机制
- 后台无审计
- 管理员权限过大
- 日志里有 PII
- 用户无法申诉
- 取消订阅过难
- 风控误伤没有复核
核心句:
伦理债和技术债一样,会积累利息。
一页总口诀
加权限前,先问最小能力。
看数据前,先问是否必要。
写日志前,先问是否敏感。
接输入前,先问是否可信。
做增长前,先问是否操控。
做推荐前,先问优化了什么。
做风控前,先问误伤怎么办。
做自动化前,先问谁能申诉。
设默认值前,先问保护谁。
做后台前,先问如何审计。
最终心法
软件系统不是中立机器。它会分配权力、塑造选择、保存记忆、制造后果。
Dimension 3 的目标,是让工程师在设计系统时,不只问:
能不能实现?
还要问:
谁获得了能力?
谁承担了风险?
谁能看到数据?
谁能纠正错误?
谁会被默认值影响?