harness 模块:AI 助手
如何向 AI 助手开放实体、会生成哪些工具,以及权限、聊天端点和 OpenAI 配置如何工作。
Harness 模块:AI 助手
harness 模块允许 AI 代理从管理面板中处理应用程序数据。
代理不会执行任意代码或任意 SQL 查询。它仅拥有一组基于应用程序模型生成的、规模小且明确的工具:
- 用于读取数据的工具,
- 用于打开表单或删除确认窗口的工具。
app/harness 包本身不了解应用程序领域。工具目录、实体字段注册表和权限规则由根据 herd/_model.goat 生成的代码提供,与仪表板的情况类似。
最重要的原则:AI 不会修改数据
Harness 工具分为两类:
- 后端 — 在服务器上执行,仅用于读取,
- 前端 — 永远不会在服务器上执行。
前端工具不会保存或删除数据。它会将代理的请求转换为浏览器操作:打开预先填充的表单,或者打开包含真实记录数据的删除确认窗口。
只有当用户亲自提交表单或确认删除时,才会发生修改。
此外,在一次对话轮次中最多只能发生一个前端操作。循环会在首次此类调用时停止,以确保不会在用户不知情的情况下发生任何操作。
相同的规则也会加入模型的系统提示词中:
- 模型不能自行创建、修改或删除数据,
- 后端工具仅用于读取,
- 模型不能编造工具名称、字段名称或记录标识符,
- 模型使用用户所用的语言回答。
为实体启用模块
模块定义
该模块在模型中定义为具有两组字段的实体模块:
entity:module:def --name harness --property:list:list=<<MODULEEOF
def --required
MODULEEOF --property:list:persist=<<MODULEEOF
def --required
MODULEEOFlist— 代理可读取的字段:由查询工具返回,并显示在删除确认窗口中,persist— 代理可在创建或编辑表单中预先填充的字段。
将模块添加到实体
通过 module:add --name=harness 将实体提供给代理。以下是实体 user 的示例:
module:add --name=harness \
--property:list:list="username,email" \
--property:list:persist="role,username,email,phone,shipping_recipient,shipping_street,shipping_unit,shipping_postal_code,shipping_city,shipping_country,billing_recipient,billing_street,billing_unit,billing_postal_code,billing_city,billing_country"persist 集合有意省略了 password 和 email_verified_at。即使用户角色拥有写入这些字段的权限,代理也不能建议这些字段的值。
请像选择 crud 模块的字段一样选择这些字段:这是对助手能够查看什么以及能够建议什么的明确决定。
对话实体
对话历史记录保存在两个模型实体中:
ai_conversation— 属于单个用户的对话,ai_message— 对话中的单条消息。
这两个实体都必须存在于 herd/_model.goat 中。它们没有 crud 模块,因此不会通过公共 CRUD API 暴露。只有 harness 端点会读取和写入它们。
仅当模型同时满足以下条件时,集成才会启用:
- 至少包含一个具有
harness模块的实体, - 包含
ai_conversation和ai_message实体。
否则,生成的 InitHarness 函数不会执行任何操作,且不会注册端点。
代码生成
修改模型后,请重新生成应用程序:
goat re生成器会创建以下文件:
goatapp/app/api/modelapi/harness_gen.go
goatapp/app/harness/store_gen.go以及每个实体的模型包中的以下掩码:
<实体>HarnessListMask
<实体>HarnessPersistMask掩码 list 始终包含字段 id,以便智能体能够指明具体记录。
请勿手动编辑这些文件。目录 goatapp/ 完全由系统生成。
生成的工具
对于每个具有模块 harness 的实体,都会生成一组三个工具:
| 工具 | 类型 | 操作 |
query_<encja> | 后端 | 读取记录以供分析。绝不修改数据。 |
open_<encja>_form | 前端 | 打开创建或编辑表单,可选择预先填充。 |
confirm_delete_<encja> | 前端 | 使用当前记录数据打开删除确认窗口。 |
例如,对于实体 product,这些工具分别为 query_product、open_product_form 和 confirm_delete_product。
query_<encja> 参数
{
"fields": ["title", "sku"],
"filters": {"status": "active"},
"limit": 20
}fields— 要返回的字段子集;省略时表示所有可用字段,filters— 按字段名称进行精确匹配的过滤条件,limit— 最大行数;默认值为 20,最大值为 50。
过滤条件仅比较相等性。不支持范围运算符、全文搜索或排序。
open_<encja>_form 参数
{
"id": "opcjonalny-identyfikator-rekordu",
"fields": {"title": "Nowy produkt"}
}- 不带
id时,将打开创建表单, - 带有
id时,将打开现有记录的编辑表单, fields包含用于预先填充的值。
confirm_delete_<encja> 参数
{"id": "identyfikator-rekordu"}字段 id 为必填项。
权限
智能体的有效访问权限始终是以下两种掩码的交集:
- 已登录用户角色的掩码,
- 该实体模块
harness的掩码。
仅有其中任意一种都不足够。代表具有 user 角色的用户运行的智能体,即使字段位于集合 list 中,也无法看到该角色无权读取的字段。同样,即使角色可以读取某些字段,也无法看到 list 之外的字段。
在实践中,这意味着:
- 实体未知的字段会被拒绝并报错,
- 用户无权访问的字段会在查询结果中被省略,
- 按无权访问的字段进行过滤会导致错误,
- 表单预填充值会通过角色的写入掩码和集合
persist进行过滤, - 删除窗口中的记录预览仅包含可读取的字段。
如果记录不存在,或者其没有任何字段可访问,预览将为空。用户无法通过这种方式区分是没有访问权限还是记录不存在。
表名和列名来自生成的注册表,并会经过验证。模型传入的值仅会作为查询参数进入 SQL。
POST /api/ai/chat 端点
该端点需要已登录的会话。
请求:
{
"conversationId": "opcjonalny-identyfikator-rozmowy",
"message": "Pokaż ostatnie zamówienia"
}响应:
{
"conversationId": "identyfikator-rozmowy",
"reply": "I've opened a form to add this product. Please review it and submit to confirm.",
"action": {
"path": "/admin/product",
"queryParams": {"aiAction": "create", "aiPrefill": "{\"title\":\"Nowy produkt\"}"}
}
}字段 action 仅在模型请求前端工具时出现。在这种情况下,reply 是由服务器生成的固定消息(目前为英文),而非模型编写的文本。
错误代码:
401— 没有已登录的会话,400— 消息为空或 JSON 无效,500— 读取历史记录或调用模型时出错。
单轮流程
对话历史 + 新消息
↓
模型选择工具
↓
后端:执行读取并返回给模型
前端:将操作返回给浏览器并结束本轮
↓
文本响应单次轮次最多可执行 5 轮后端工具调用。超过该限制后,端点会返回错误,以防模型陷入无限循环。
对话历史
仅保存用户和助手消息。中间工具调用仅存在于单个轮次内,不会传递到后续请求。
对话属于创建它的用户。提供 conversationId 的他人对话会被视为该对话不存在。
前端
仪表盘小部件
聊天是一个类型为 ai_chat 的仪表盘小部件。在此应用中,它定义于 herd/_model.goat:
section:add --name=assistant --type=cards --roles=admin,manager,user
widget:add --name=ai_assistant --type=ai_chat --label="AI Assistant"ai_chat 小部件不接受数据源、聚合、图表或查询。它仅与 /api/ai/chat 交互。
执行操作
当响应包含 action 时,AiChatService 服务会跳转到生成的实体视图:
/admin/<encja-w-kebab-case>并携带以下参数:
aiAction—create、edit或delete,aiPrefill— 用于预填创建表单的值的 JSON,aiId— 要编辑或删除的记录标识符,aiPreview— 用于删除确认窗口中记录预览的 JSON。
生成的实体管理器会读取这些参数,打开相应的表单或窗口,并从 URL 中移除这些参数。
预填仅适用于创建表单。对于 edit,管理器会根据 aiId 打开现有记录,并显示其当前值。
配置
OpenAI 客户端通过环境变量配置:
| 变量 | 含义 | 默认值 |
GOAT_AI_OPENAI_API_KEY | 助手使用的 OpenAI API 密钥 | 无 |
GOAT_AI_OPENAI_MODEL | 助手使用的聊天模型 | gpt-4o-mini |
如果密钥为空,端点仍会被注册,但每个请求都会以错误结束,小部件将显示助手不可用的消息。
不要将这些变量与用于翻译脚本的 GOAT_OPENAI_API_KEY 混淆(herd/translate.goat、translate-docs.goat)。
该密钥是机密信息。请将其存储在本地 .env 或部署配置中,且绝不要将其提交到仓库。herd/deploy.goat 将 GOAT_AI_OPENAI_API_KEY 声明为机密,并将 GOAT_AI_OPENAI_MODEL 声明为普通变量。
数据隐私
后端工具结果会作为对话的一部分发送给模型。因此,用户可读取的 list 集合中的所有字段都可能被发送到 OpenAI。
将字段添加到 list 即表示决定将其提供给外部 AI 服务商。请勿在其中放入密码、令牌或不得在应用外处理的数据。
测试
该包的逻辑在不连接 OpenAI 的情况下进行测试。Run 函数接受 Completer 接口,测试会用伪客户端替代它:
(cd goatapp && GOWORK=off go test ./app/harness/)生成的 E2E 测试 goatapp/web/e2e-server/harness.spec.ts 始终会验证匿名请求(401)和空消息(400)会被拒绝。
完整对话会调用已配置的 AI 服务商,因此仅在运行 Playwright 测试时显式设置变量后才会执行:
GOAT_E2E_HARNESS_CHAT=1总结
harness模块通过三个生成的工具向 AI 助手公开实体。- 读取受角色掩码与
list集合的合取限制。 - 表单预填充受角色写入掩码与
persist集合的限制。 - 助手绝不会自行写入或删除数据;始终由人工执行。
- 运行需要
ai_conversation和ai_message实体,以及GOAT_AI_OPENAI_API_KEY密钥。