feat: implement instruction assembly and project instruction providers with tests

This commit is contained in:
mlogclub
2026-04-14 12:00:03 +08:00
parent 9c7d70741a
commit 03fc405e4e
12 changed files with 58 additions and 129 deletions
@@ -1,4 +1,4 @@
package factory package instruction
import "strings" import "strings"
@@ -8,11 +8,11 @@ const defaultGovernanceInstruction = `
如果存在工具白名单限制只能调用当前允许的工具信息不足时优先追问不要伪造事实或跳过必要确认 如果存在工具白名单限制只能调用当前允许的工具信息不足时优先追问不要伪造事实或跳过必要确认
` `
type InstructionAssembler struct { type Assembler struct {
governanceInstruction string governanceInstruction string
} }
type InstructionAssemblerInput struct { type AssemblerInput struct {
AgentInstruction string AgentInstruction string
GovernanceInstruction string GovernanceInstruction string
SkillInstruction string SkillInstruction string
@@ -20,8 +20,7 @@ type InstructionAssemblerInput struct {
ProjectInstruction string ProjectInstruction string
} }
// InstructionAssemblySummary 描述 instruction 各组成部分的来源摘要。 type AssemblySummary struct {
type InstructionAssemblySummary struct {
SectionTitles []string SectionTitles []string
HasProjectRule bool HasProjectRule bool
HasGovernanceRule bool HasGovernanceRule bool
@@ -30,26 +29,22 @@ type InstructionAssemblySummary struct {
HasToolRule bool HasToolRule bool
} }
// InstructionAssemblyResult 为 instruction 装配结果。 type AssemblyResult struct {
type InstructionAssemblyResult struct {
Text string Text string
Summary InstructionAssemblySummary Summary AssemblySummary
} }
func NewInstructionAssembler() *InstructionAssembler { func NewAssembler() *Assembler {
return &InstructionAssembler{ return &Assembler{governanceInstruction: strings.TrimSpace(defaultGovernanceInstruction)}
governanceInstruction: strings.TrimSpace(defaultGovernanceInstruction),
}
} }
func (a *InstructionAssembler) Build(input InstructionAssemblerInput) string { func (a *Assembler) Build(input AssemblerInput) string {
return a.Assemble(input).Text return a.Assemble(input).Text
} }
// Assemble 构建最终 instruction 文本及其来源摘要。 func (a *Assembler) Assemble(input AssemblerInput) AssemblyResult {
func (a *InstructionAssembler) Assemble(input InstructionAssemblerInput) InstructionAssemblyResult {
parts := make([]string, 0, 5) parts := make([]string, 0, 5)
summary := InstructionAssemblySummary{SectionTitles: make([]string, 0, 5)} summary := AssemblySummary{SectionTitles: make([]string, 0, 5)}
projectInstruction := strings.TrimSpace(input.ProjectInstruction) projectInstruction := strings.TrimSpace(input.ProjectInstruction)
if projectInstruction == "" { if projectInstruction == "" {
projectInstruction = strings.TrimSpace(DefaultProjectInstruction) projectInstruction = strings.TrimSpace(DefaultProjectInstruction)
@@ -83,7 +78,7 @@ func (a *InstructionAssembler) Assemble(input InstructionAssemblerInput) Instruc
summary.HasToolRule = true summary.HasToolRule = true
summary.SectionTitles = append(summary.SectionTitles, "工具补充规则") summary.SectionTitles = append(summary.SectionTitles, "工具补充规则")
} }
return InstructionAssemblyResult{ return AssemblyResult{
Text: strings.TrimSpace(strings.Join(parts, "\n\n")), Text: strings.TrimSpace(strings.Join(parts, "\n\n")),
Summary: summary, Summary: summary,
} }
@@ -1,12 +1,12 @@
package factory package instruction
import ( import (
"strings" "strings"
"testing" "testing"
) )
func TestInstructionAssemblerRespectsProvidedSources(t *testing.T) { func TestAssemblerRespectsProvidedSources(t *testing.T) {
result := NewInstructionAssembler().Assemble(InstructionAssemblerInput{ result := NewAssembler().Assemble(AssemblerInput{
ProjectInstruction: "project-rule", ProjectInstruction: "project-rule",
GovernanceInstruction: "governance-rule", GovernanceInstruction: "governance-rule",
AgentInstruction: "agent-rule", AgentInstruction: "agent-rule",
@@ -1,4 +1,4 @@
package factory package instruction
import ( import (
"encoding/json" "encoding/json"
@@ -10,7 +10,7 @@ import (
"cs-agent/internal/pkg/toolx" "cs-agent/internal/pkg/toolx"
) )
func buildSelectedSkillActivationInstruction(skill *models.SkillDefinition) string { func BuildSelectedSkillActivationInstruction(skill *models.SkillDefinition) string {
if skill == nil { if skill == nil {
return "" return ""
} }
@@ -26,7 +26,7 @@ func buildSelectedSkillActivationInstruction(skill *models.SkillDefinition) stri
return strings.TrimSpace(strings.Join(lines, "\n")) return strings.TrimSpace(strings.Join(lines, "\n"))
} }
func buildSelectedSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) string { func BuildSelectedSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) string {
if skill == nil { if skill == nil {
return "" return ""
} }
@@ -0,0 +1,15 @@
package instruction
// DefaultProjectInstruction 为当前项目统一注入的全局项目规则。
//
// 默认回退到内置常量;运行时优先由 ProjectInstructionProvider 读取仓库中的 AGENTS.md。
const DefaultProjectInstruction = `# AGENTS.md
本文件定义本项目内 AI Agent 的强制开发规则。除非用户明确要求偏离,否则必须遵循。
## 1. 基本原则
- 适用范围:仓库根目录及所有子目录
- 优先级:用户明确指令 > 本文件 > 默认实现习惯
- 若与用户要求冲突:先执行用户要求,并在变更说明中标注偏离点
`
@@ -1,4 +1,4 @@
package factory package instruction
import ( import (
"os" "os"
@@ -14,7 +14,6 @@ type ProjectInstructionProvider struct {
fileName string fileName string
} }
// TODO 这个要读取AGENTS.md文件,后面考虑还要不要
func NewProjectInstructionProvider() *ProjectInstructionProvider { func NewProjectInstructionProvider() *ProjectInstructionProvider {
return &ProjectInstructionProvider{fileName: "AGENTS.md"} return &ProjectInstructionProvider{fileName: "AGENTS.md"}
} }
@@ -84,7 +83,7 @@ func NewSkillInstructionProvider() *SkillInstructionProvider {
} }
func (p *SkillInstructionProvider) Resolve(selectedSkill *models.SkillDefinition) string { func (p *SkillInstructionProvider) Resolve(selectedSkill *models.SkillDefinition) string {
return buildSelectedSkillActivationInstruction(selectedSkill) return BuildSelectedSkillActivationInstruction(selectedSkill)
} }
func (p *ToolAppendixProvider) Build(toolDefinitions []runtimetooling.MCPToolDefinition, extraToolCodes map[string]string) []string { func (p *ToolAppendixProvider) Build(toolDefinitions []runtimetooling.MCPToolDefinition, extraToolCodes map[string]string) []string {
@@ -1,4 +1,4 @@
package factory package instruction
import ( import (
"os" "os"
@@ -1,4 +1,4 @@
package factory package instruction
import ( import (
"strings" "strings"
@@ -7,23 +7,23 @@ import (
"cs-agent/internal/models" "cs-agent/internal/models"
) )
type InstructionService struct { type Service struct {
assembler *InstructionAssembler assembler *Assembler
projectInstructionProvider *ProjectInstructionProvider projectInstructionProvider *ProjectInstructionProvider
governanceInstructionProvider *GovernanceInstructionProvider governanceInstructionProvider *GovernanceInstructionProvider
skillInstructionProvider *SkillInstructionProvider skillInstructionProvider *SkillInstructionProvider
toolAppendixProvider *ToolAppendixProvider toolAppendixProvider *ToolAppendixProvider
} }
func NewInstructionService( func NewService(
assembler *InstructionAssembler, assembler *Assembler,
projectProvider *ProjectInstructionProvider, projectProvider *ProjectInstructionProvider,
governanceProvider *GovernanceInstructionProvider, governanceProvider *GovernanceInstructionProvider,
skillProvider *SkillInstructionProvider, skillProvider *SkillInstructionProvider,
toolProvider *ToolAppendixProvider, toolProvider *ToolAppendixProvider,
) *InstructionService { ) *Service {
if assembler == nil { if assembler == nil {
assembler = NewInstructionAssembler() assembler = NewAssembler()
} }
if projectProvider == nil { if projectProvider == nil {
projectProvider = NewProjectInstructionProvider() projectProvider = NewProjectInstructionProvider()
@@ -37,7 +37,7 @@ func NewInstructionService(
if toolProvider == nil { if toolProvider == nil {
toolProvider = NewToolAppendixProvider() toolProvider = NewToolAppendixProvider()
} }
return &InstructionService{ return &Service{
assembler: assembler, assembler: assembler,
projectInstructionProvider: projectProvider, projectInstructionProvider: projectProvider,
governanceInstructionProvider: governanceProvider, governanceInstructionProvider: governanceProvider,
@@ -46,12 +46,12 @@ func NewInstructionService(
} }
} }
func (s *InstructionService) Build( func (s *Service) Build(
aiAgent *models.AIAgent, aiAgent *models.AIAgent,
selectedSkill *models.SkillDefinition, selectedSkill *models.SkillDefinition,
toolDefinitions []runtimetooling.MCPToolDefinition, toolDefinitions []runtimetooling.MCPToolDefinition,
extraToolCodes map[string]string, extraToolCodes map[string]string,
) InstructionAssemblyResult { ) AssemblyResult {
baseInstruction := "" baseInstruction := ""
if aiAgent != nil { if aiAgent != nil {
baseInstruction = strings.TrimSpace(aiAgent.SystemPrompt) baseInstruction = strings.TrimSpace(aiAgent.SystemPrompt)
@@ -72,11 +72,11 @@ func (s *InstructionService) Build(
if s != nil && s.toolAppendixProvider != nil { if s != nil && s.toolAppendixProvider != nil {
toolAppendices = s.toolAppendixProvider.Build(toolDefinitions, extraToolCodes) toolAppendices = s.toolAppendixProvider.Build(toolDefinitions, extraToolCodes)
} }
assembler := NewInstructionAssembler() assembler := NewAssembler()
if s != nil && s.assembler != nil { if s != nil && s.assembler != nil {
assembler = s.assembler assembler = s.assembler
} }
return assembler.Assemble(InstructionAssemblerInput{ return assembler.Assemble(AssemblerInput{
AgentInstruction: baseInstruction, AgentInstruction: baseInstruction,
GovernanceInstruction: governanceInstruction, GovernanceInstruction: governanceInstruction,
SkillInstruction: skillInstruction, SkillInstruction: skillInstruction,
@@ -4,6 +4,7 @@ import (
"context" "context"
"strings" "strings"
runtimeinstruction "cs-agent/internal/ai/runtime/instruction"
einoagents "cs-agent/internal/ai/runtime/internal/impl/agents" einoagents "cs-agent/internal/ai/runtime/internal/impl/agents"
einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks" einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks"
"cs-agent/internal/ai/runtime/registry" "cs-agent/internal/ai/runtime/registry"
@@ -18,7 +19,7 @@ import (
type AgentFactory struct { type AgentFactory struct {
chatModelFactory *ChatModelFactory chatModelFactory *ChatModelFactory
toolFactory *ToolFactory toolFactory *ToolFactory
instructionService *InstructionService instructionService *runtimeinstruction.Service
handlerService *AgentHandlerService handlerService *AgentHandlerService
} }
@@ -55,7 +56,7 @@ func NewAgentFactory() *AgentFactory {
return &AgentFactory{ return &AgentFactory{
chatModelFactory: NewChatModelFactory(), chatModelFactory: NewChatModelFactory(),
toolFactory: NewToolFactory(), toolFactory: NewToolFactory(),
instructionService: NewInstructionService(nil, nil, nil, nil, nil), instructionService: runtimeinstruction.NewService(nil, nil, nil, nil, nil),
handlerService: NewAgentHandlerService(nil), handlerService: NewAgentHandlerService(nil),
} }
} }
@@ -1,85 +0,0 @@
package factory
// DefaultProjectInstruction 为当前项目统一注入的全局项目规则。
//
// 默认回退到内置常量;运行时优先由 ProjectInstructionProvider 读取仓库中的 AGENTS.md。
// TODO 这个内容不合适,需要重新整理内容,内容需要面向客服系统
const DefaultProjectInstruction = `# AGENTS.md
本文件定义本项目内 AI Agent 的强制开发规则。除非用户明确要求偏离,否则必须遵循。
## 1. 基本原则
- 适用范围:仓库根目录及所有子目录
- 优先级:用户明确指令 > 本文件 > 默认实现习惯
- 若与用户要求冲突:先执行用户要求,并在变更说明中标注偏离点
## 2. 固定技术栈
- 后端:Golang + Iris + GORM + github.com/mlogclub/simple
- 数据库:同时兼容 SQLite 和 MySQL
- 前端:Next.js(App Router) + React + shadcn/ui + Tailwind CSS
- 前端包管理器:pnpm
## 3. 后端分层
必须遵循单向依赖:models -> repositories -> services -> controllers
- models:只定义实体和表映射
- repositories:只封装数据访问
- services:负责业务规则、事务编排、聚合逻辑
- controllers:只做参数解析、权限校验、service 调用、响应封装
禁止:
- controller 直接调用 repository
- 直接将 GORM model 返回前端
- 在 models/repositories 中写业务编排
## 4. simple 使用约定
- DB 初始化后必须执行:sqls.SetDB(db)
- 查询条件优先使用:sqls.Cnd
- 参数绑定优先使用:web/params
- HTTP 响应统一使用:web.JsonData、web.JsonPageData、web.JsonError
## 5. 数据库兼容规则
- 字段类型使用兼容集合:varchar、text、int、bigint、datetime
- 主键统一使用 int64
- 避免数据库私有语法和方言特性
## 6. 接口与 DTO
- DTO 分离:request / response 分开定义
- JSON 字段统一使用 camelCase
- 禁止透传底层 SQL 错误
- controller 入参使用 request DTO
- controller 出参使用 response DTO
- 禁止直接返回 models 到前端
## 7. Go 代码规范
- 日志统一使用标准库 log/slog
- 新增 Go 代码统一使用 any,禁止新增 interface{}
- 修改 Go 代码后必须执行 gofmt
## 8. 前端规范
- 前端目录:web
- 框架:Next.js 16 + App Router
- 基础组件优先使用 shadcn/ui
- 前端业务接口统一通过 web/lib/api/* 发起,禁止页面里散落裸 fetch
- 新增或修改前端页面后至少执行:cd web && pnpm typecheck
## 9. 提交前检查清单
每次修改后至少确认:
1. 没有跨层调用或反向依赖
2. 写操作有明确事务边界
3. 返回仍符合统一 JsonResult 结构
4. 兼容 SQLite 与 MySQL
5. 补充了必要测试,至少覆盖 service 核心路径
6. Go 改动已执行 gofmt
7. 前端改动至少通过 pnpm lint 或 pnpm typecheck(在 web 目录)`
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
runtimeinstruction "cs-agent/internal/ai/runtime/instruction"
runtimetooling "cs-agent/internal/ai/runtime/tooling" runtimetooling "cs-agent/internal/ai/runtime/tooling"
"cs-agent/internal/models" "cs-agent/internal/models"
@@ -25,7 +26,7 @@ func newSelectedSkillBackend(selectedSkill *models.SkillDefinition, toolDefiniti
return nil, fmt.Errorf("selected skill code is empty") return nil, fmt.Errorf("selected skill code is empty")
} }
description := strings.TrimSpace(selectedSkill.Description) description := strings.TrimSpace(selectedSkill.Description)
content := buildSelectedSkillDocument(selectedSkill, toolDefinitions) content := runtimeinstruction.BuildSelectedSkillDocument(selectedSkill, toolDefinitions)
return &selectedSkillBackend{ return &selectedSkillBackend{
frontMatter: einoskill.FrontMatter{ frontMatter: einoskill.FrontMatter{
Name: skillName, Name: skillName,
@@ -3,6 +3,7 @@ package factory
import ( import (
"strings" "strings"
runtimeinstruction "cs-agent/internal/ai/runtime/instruction"
einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks" einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks"
"cs-agent/internal/ai/runtime/registry" "cs-agent/internal/ai/runtime/registry"
runtimetooling "cs-agent/internal/ai/runtime/tooling" runtimetooling "cs-agent/internal/ai/runtime/tooling"
@@ -10,7 +11,7 @@ import (
"cs-agent/internal/pkg/toolx" "cs-agent/internal/pkg/toolx"
) )
func buildInstructionTraceSummary(summary InstructionAssemblySummary) einocallbacks.InstructionTraceSummary { func buildInstructionTraceSummary(summary runtimeinstruction.AssemblySummary) einocallbacks.InstructionTraceSummary {
return einocallbacks.InstructionTraceSummary{ return einocallbacks.InstructionTraceSummary{
SectionTitles: append([]string(nil), summary.SectionTitles...), SectionTitles: append([]string(nil), summary.SectionTitles...),
HasProjectRule: summary.HasProjectRule, HasProjectRule: summary.HasProjectRule,
@@ -2,10 +2,12 @@ package factory
import ( import (
"testing" "testing"
runtimeinstruction "cs-agent/internal/ai/runtime/instruction"
) )
func TestBuildInstructionTraceSummary(t *testing.T) { func TestBuildInstructionTraceSummary(t *testing.T) {
got := buildInstructionTraceSummary(InstructionAssemblySummary{ got := buildInstructionTraceSummary(runtimeinstruction.AssemblySummary{
SectionTitles: []string{"项目级规则", "当前技能上下文"}, SectionTitles: []string{"项目级规则", "当前技能上下文"},
HasProjectRule: true, HasProjectRule: true,
HasGovernanceRule: true, HasGovernanceRule: true,