github开源地址:jeokeo011222/quantbot: QuantBot(AI 量化机器人)的全量源码。基于 **Go+React + TypeScript + Vite** 构建,通过 **Wails** 与 Go 后端通信,提供 A 股量化交易的图形界面:六维判势、多因子选股、AI 智能体决策、策略回测、实时活动、审计日志等。
2.1同一条线,写在三个地方,三个数字
二零二六年九月,我做了一次很普通的核对:把几处定义“风控硬限制”的代码并排打开。 系统里有两条硬限制:一只股票最多能占组合多大比例,以及组合最多能容忍多深的回撤。核对结果让我不太舒服——同一条限制,在不同的地方写着不同的数字。配置默认值里,回撤容忍写的是 20%默认配置internal/config/config.go:MaxPositionSize=0.30、MaxDrawdownPct=0.20。核对时间 2026-09-24。1;策略引擎的硬限制结构里,同一个回撤容忍写的是 10%策略引擎internal/policy/engine.go的HardLimits:MaxSinglePosition=0.30、MaxDrawdown=0.10。核对时间 2026-09-24。2;到了风险中心的合规校验里,30% 与 10% 又被当作文案参数各自写死了一遍风险中心internal/riskcenter/riskcenter.go的校验项把 0.30 与 0.10 硬编码进提示文案(第 233、247 行)。核对时间 2026-09-24。3。 我的第一反应是抄写错误。抄错了,改回来就行。 第二反应让我坐了两个小时没动:一笔买入在下单之前,只会走过其中一条路径。它被哪一处检查,取决于它是从哪个入口发起的。也就是说,同一笔交易换个入口,面对的就是另一条线。 这几处数字里,最多只有一处是当前的真实口径。项目后来把口径统一成单票 30%、回撤 10%。 这件事在项目里被记成“风控硬限制形同虚设”同上,记为项目真实修复记录第 7 条。4。我一开始把它当作数值一致性的问题,后来发现完全不是——它是一个权限问题。 因为那条线之所以会被写成三个数,根本原因是:这条线没有主人。谁都能定义它,谁都不用为它负责。而一条没有主人的线,最后一定会被走它的人改掉。 我在这里第一次想清楚一件事,它后来变成了整本书反复出现的一条原则:如果一个人既能修改这条线,又能执行下单,那么这条线存不存在,只取决于他的自觉。用财务的话说:让管账的人自己去提钱——账本是平的,钱也是没的。
2.2两个生活里的“不”
讲权限很容易写成条文。我们先绕开条文,去看两个真实存在的地方:它们都不允许“一个人说了算”,而且理由不是道德,是结构。
一次航班起飞前,至少有三个人可以拦下它。 机长可以决定今天飞不飞;签派员可以因为航路天气说“今天不放行”;加油没加够、配载没算好,机长再想飞也飞不起来。 三个“不”性质完全不同。机长判断的是“能不能飞得成”,签派判断的是“今天允不允许飞”,配载判断的是“这架飞机现在的状态对不对”。 关键不在谁官大,而在于:说“不”的权力,被分发给了那些不靠“这趟飞成功”来领奖金的人。如果签派员和机长是同一个人、拿同一份奖金,那么恶劣天气下“能不能飞”这个判断,一定会被“这趟必须飞”的动机污染。
一家公司管钱,其实只有两个动作:记账,和付款。 如果这两个动作由同一个人做,“账上还剩多少”和“我现在能付多少”就变成了同一个脑子里的两个念头。他不会先记完账再决定付不付——他会一边付,一边给账本找一个说法。 分开之后,两个人各自都不完整:会计不能说“钱够”,出纳不能说“账对”。谁都不能单独把一件事办成,但谁都能单独把一件事拦下。 这就是分工真正的作用。它不产生能力,它产生否决。
第一个类比回答“为什么要说不的权力分出去”,第二个回答“分出去之后会变成什么样”。两句话合起来,就是本章要落到代码里的东西。
2.3如果不这么做,会怎样
有人会说:我知道不能让同一个人既管账又提钱,那我就在提示词里写一句“你不许同时做这两件事”,不就完了? 我在最早期那版设计里,权限就是“一张清单”:每个角色下面列几条“它能做什么”。清单写在系统提示词里,措辞也算清楚。 它失效的方式有三种,而且都不报错。第一种,模型会改口。同一个角色,上下文里被塞进更多信息之后,“我能不能做这件事”的判断会漂移。今天它认为“我只是建议一下”,明天它认为“我这是在帮 CIO 拍板”。清单是一句形容词,形容词没有边界。第二种,多轮累积。一次越权不致命。致命的是越权的结果变成了下一轮的输入。“我已经定下这只股票”这句话一旦出现,后面的风控环节就会围着它打转。第三种,没人能验证。提示词写在代码里,本质是一次字符串拼接。跑完之后,日志里只有它说的话,没有“它当时有没有资格说这句话”的记录。你想查它有没有越权,只能把那段文字重读一遍,然后自己判断。 第三种最要命。它意味着:权限这件事在系统里没有任何一个地方被“计算”过,全都靠人读。
2.4从“职责描述”到“权限矩阵”
先给一个“差不多对”的模型:分工就是谁负责什么。它对了一半。它的问题是:“负责什么”是名词,而权限是动词。“风控师负责风险”是名词。它能被解释成一百件事:评估风险、决定不买、建议减仓、直接卖掉一半。“能不能提交一张卖单”是动词,只有两个答案。 近似模型在这里失效了——颗粒度不同。名词回答不了“具体哪个动作可以、哪个不行”。所以权限不能写成职责,只能写成“角色 × 动作”的格子。 严谨版本长这样。
定义 2.1(权限矩阵)
设 为角色集合、 为动作集合。权限矩阵(permission matrix) 是一个映射允许拒绝允许拒绝
只有当 允许允许 时,角色 才能执行动作 ;未在表中显式列出的 ,一律按拒绝处理。
一句话解释:这张表不是“每个角色能做什么”的说明书,而是“某个角色做某个动作时,系统放不放行”的开关。表里没有的格子,默认是关着的。
命题 2.2(默认拒绝)
若权限表里既没有角色 与动作 的记录,默认矩阵的 行里也没有 ,则系统判定的结果是不允许。
这个命题在说什么:权限系统的默认答案必须是“不”,做不到这一点的权限系统等于没有权限。
证明■
系统的权限查询按固定顺序进行(internal/agentworkflow/permissions.go): (1)先在权限表中按 查询。若命中一条记录,就直接返回该记录里的布尔值,查询结束。 (2)若查询结果为“没有这条记录”,进入回退分支:取出默认矩阵中 对应的那一行;若该行不存在,或该行里没有键 ,返回 。 (3)只有当“行存在且键 存在”时,才返回该键的布尔值。 于是“既无记录、也未列出”这一情形必然走到返回 的那一条语句上。命题成立。
近似模型的第二处失效:它管不了新来的角色。如果我明天加第六个岗位,一个只写职责的模型不会告诉我它默认该有哪些权限;矩阵模型的答案却是确定的——一行都不加,等于什么都没有。 第三处失效最深:近似模型看不见“谁能否决谁”。这是名词清单天生缺的东西,下面单独说。
2.5五条线:它在系统里长什么样
现在把话说准。这五条线,第一条是流水线,第二条是权限矩阵,第三条是三句写死在代码里的硬约束。
2.5.1第一条线:流水线
和第 第一章 章那张流水线图相比,这张图每一格下面多了一个动词。这不是修辞:这五个动词在系统里是有名字的,它们决定每个角色在这条链上只能推进哪一步。
| 角色 | 链上动作 | 系统里对应的一条“不能” |
| AgentPlanner | PROPOSE | 在默认矩阵里挂着“禁止批准投资”(PermForbidApproveInvestment)与“禁止执行订单”(PermForbidExecuteOrder) |
| AgentQuant | EVIDENCE | 同上两条;它只有推荐类权限(推荐资产 / 推荐因子 / 推荐组合 / 生成候选),没有一条决策类权限 |
| AgentRisk | VETO | 唯一允许否决的角色;它的权限里包含“风险否决”(PermRiskReject)与“紧急冻结”(PermEmergencyFreeze) |
| AgentCIO | DECIDE | 它的权限行里没有任何一条订单类权限,也没有“禁止执行订单”这一条——因为它不需要禁止,它本来就不在订单路径上 |
| AgentTrader | EXECUTE | 挂着三条禁止:禁止选股(PermForbidSelectStock)、禁止改目标权重(PermForbidChangeTargetWeight)、禁止绕过风控(PermForbidOverrideRisk) |
前四列来自默认权限矩阵PermissionMatrix;“紧急冻结”等权限名见同一文件的权限常量区。
系统源码internal/agentworkflow/models.go(PermissionMatrix与权限常量,第 49--143 行);核对时间 2026-09-24。
看第四列。系统没有只写“能做什么”,它同时写下了“不能做什么”,而且这些“不能”被做成了有名字的权限项。 这是个很实用的工程决定:如果“不能做”只是一句注释,它就会随代码一起烂掉;把它做成一条权限记录,它就变成了一个可以查、可以测、可以被审计的对象。
2.5.2第二条线:权限矩阵
代码 2.1权限矩阵里 TRADER 与 CIO 两行(节选)
AgentCIO: { PermSelectAsset: true, // 选择标的 PermSetTargetWeight: true, // 设定目标权重 PermApproveInvestment: true, // 批准投资 PermChangeInvestmentDecision: true, // 变更投资决策 // ... 另有 增/减/清仓、批准/拒绝投资方案 等 }, AgentTrader: { PermCreateOrder: true, // 创建订单 PermSubmitOrder: true, // 提交订单 PermMonitorOrder: true, // 监控订单 PermCancelOrder: true, // 撤单 PermReconcile: true, // 对账 PermForbidSelectStock: true, // 禁止选股 PermForbidChangeTargetWeight: true, // 禁止修改目标权重 PermForbidOverrideRisk: true, // 禁止绕过风控 },这段话在说什么:CIO 那一行全是“决定什么”,Trader 那一行全是“执行什么”,两行的交集是空的。你把两张表并排看一遍,就能回答“谁能下单、谁能选股”——不需要读任何一段自然语言。
这张表最容易被忽略的一行
规划师那一行里挂着一条PermRiskApprove(风险批准)。按它当前的三项职责(创建画像、修改目标、创建方案),这条记录并不使用。 我把它写出来不是为了挑刺,而是想说一件很实在的事:真实系统的权限表会漂移。所以判断权限不能只看“设计意图”,必须看“表里到底写了什么”——这也正是第 2.7 节那份“动手试一试”要你做的事。
前端把这件事做成了一张 8 个操作权限 × 5 个角色的矩阵表格,绿色表示允许、灰色表示不允许docs/用户操作手册.md第 5 节“AI 团队页”;该系统事实清单第 3 节记为“8 个操作权限 × 5 个角色”。核对时间 2026-09-24。5。代码里的默认矩阵是按角色分行、按权限分列的另一张表,条目比 8 条多——CIO 一行就有 9 条。两张表的用途不同:代码那张用来判权,前端那张用来给人看。
2.5.3第三条线:三句写死的硬约束
权限表是可以改的:它在数据库里有一张表,能授、能撤。但有三件事不在那张表里。代码 2.2三条硬约束(internal/agentworkflow/permissions.go,原文)
// CanExecuteOrder 检查智能体是否可以执行订单 func (pm *PermissionManager) CanExecuteOrder(agentID string) bool { // 只有Trader可以执行订单 return agentID == AgentTrader } // CanSelectStock 检查智能体是否可以选择股票 func (pm *PermissionManager) CanSelectStock(agentID string) bool { // 只有CIO可以正式选入股票 return agentID == AgentCIO } // HasVetoPower 检查智能体是否有否决权 func (pm *PermissionManager) HasVetoPower(agentID string) bool { // 只有Risk有否决权 return agentID == AgentRisk }这段话在说什么:这三个函数里没有数据库查询。它们不读权限表,不看默认矩阵,只做一次身份比较。这意味着——“谁能下单”“谁能选股”“谁能否决”,不是配置项,是常量。你把数据库里 Trader 的所有权限记录都改成“禁止”,这三条依然成立。这不是偷懒,这是刻意的:这三件事动不得。
命题 2.3(三条硬约束互斥)
对任何一个角色 ,“可执行订单”“可选择股票”“有否决权”这三条硬约束中,至多只有一条成立。
这个命题在说什么:权力是分给三个不同的人的,不会出现一个人同时握着“下单”和“否决”。
证明■
由上一段源码:判定“可执行订单”成立的充要条件是 ;判定“可选择股票”成立的充要条件是 ;判定“有否决权”成立的充要条件是 。 在internal/agentworkflow/models.go中,AgentTrader、AgentCIO、AgentRisk是三个取不同字符串值的常量(分别对应操盘手、首席投资官、风控师)。 于是三个判定各自成立时, 必须分别等于三个互不相同的常量,故不可能同时取到两个。命题成立。
把这个命题和第 2.2 条放一起看,本章的主张就完整了:默认拒绝保证了“没写的就是不许”,三条硬约束保证了“最要紧的三件事连改都不许改”。
2.6常见误解与纠偏
我在最初那版提示词里写过:“你是风控师,不要越权执行交易。” 这句话的实际效果是零。因为模型的输出空间没有任何一处被裁剪过——它依然可以生成一句“我决定卖出这只股票”,而这句话会被下游当成一段文本读进去。 后来的做法是反过来:不再描述边界,而是把门装上。下单要经过CanExecuteOrder;选股要经过CanSelectStock;否决要走HasVetoPower。判断一个系统有没有权限,不看它提示词写得多严,看它有没有一个会返回 false 的函数。
这是我推翻得最彻底的一个认知。 我一开始觉得,风控师的否决权是一种“建议”:它说了不行,CIO 想想办法还是能过。如果真是这样,那风控就是一块装饰。 但这个系统里的否决权,位置上比决策权更高——因为决策权只能决定“我打算做什么”,否决权能决定“这个打算能不能变成真的”。CIO 拍板一个方案,风控师说不,这个方案就停在原地。反过来不成立:风控师不能说“我要买”,它没有下单权限,也没有选股权限。 一条只能收紧、不能放松的权力,比一条既能收紧也能放松的权力更可靠。第 第二十二章 章会把这个结构再讲一遍——那里的主角换成了规则,但骨架完全一样。
“能授能撤”听起来像件好事,但它会让人产生一个错误的安全感:以为把权限放进数据库,就等于权限被管住了。 这个系统里有两条防线,性质完全不同:数据库里的权限是可以被改的——这是它的用途,也是它的风险点。撤掉某条权限,立刻生效;但如果有人误操作,也同样立刻生效。那三条硬约束改不动。它们是一句身份比较,不查表。 我后来给自己定的规矩是:凡是“失去了会出人命”的权限,就不放进可配置的那一层。下单、否决、选股权,都属此类。
2.7真实出处、动手试一试与下一章
- 权限查询顺序与“默认拒绝”回退:
internal/agentworkflow/permissions.go(CheckPermission,第 21--37 行) - 三条硬约束的实现:同文件
CanExecuteOrder/CanSelectStock/HasVetoPower(第 89--105 行) - 默认权限矩阵与全部权限常量:
internal/agentworkflow/models.go(第 49--143 行) - 默认数据的落库(把默认矩阵逐条写入权限表):同文件
InitDefaultAgentData - 前端“AI 团队”页的 8 × 5 权限矩阵与“可做 / 不可做”清单:
ui/src/pages/AITeam.tsx,路由表ui/src/App.tsx
动手试一试(五分钟)
- 打开系统,进入左侧菜单的AI 团队页。
- 展开 PLANNER,把它“不能做”的三条抄下来;再展开 TRADER,把它“不能做”的三条抄下来。
- 回到权限矩阵那张表,找三行:执行订单、否决决策、选择股票。看清每一行打了几个绿勾。
- 最后自己回答一句:如果明天系统新增第六个岗位“客服”,而你只写了一句职责描述、没有给它任何一行权限,它能不能下单?(答案在第 2.2 条命题里。)
五条线画完了。但“谁不能做什么”只解决了一半问题:同一句“看看今天的市场”,交到两个人手里,会被做成两份完全不同的东西——一份是一段市场评论,一份是七个字段的证据清单。有了权力边界,还缺一份派活的标准格式。这就是下一章要给每个岗位发的东西:一份写明了“必须做什么、禁止做什么、产出长什么样、什么时候必须停”的合同。