AI功能配置系统详细指南
本文档详细说明X-Wukong Mini Backend的AI功能配置逻辑、工作流程和设计模式。
目录
一、AI功能配置系统架构
1. 核心数据模型(model/ai_function.go)
AIFunction - AI功能配置表
这是整个系统的核心配置实体,包含以下关键字段:
功能标识
function_key: 功能唯一标识(如ai_topic_evaluation)function_name: 功能显示名称description: 功能描述
提示词系统
prompt_template: 发送给AI的完整提示词模板,支持变量占位符- 例如:
{{subject}},{{title}} - 包含详细的指令、格式要求、示例等
- 例如:
user_prompt_template: 展示给用户的简化版引导语- 更简洁,便于用户理解
- 同样支持变量占位符
字段配置系统
input_fields: JSONB格式的输入字段配置数组- 定义用户需要提供的输入参数
- 包含验证规则
output_fields: JSONB格式的输出字段配置数组- 定义AI返回数据的结构
- 支持预览字段标记
输出格式控制
output_format: 支持4种类型json_object: 结构化对象(默认)json_array: 数组列表plain_text: 纯文本markdown: Markdown格式
模型配置
model_id: AI模型ID(外键)model_config: JSONB格式的模型参数覆盖- 可覆盖默认的temperature、max_tokens等参数
状态管理
status: 状态(0=未启动,1=启用,2=禁用)sort: 排序值
AIFieldConfig - 字段配置结构
灵活的字段定义系统,支持以下属性:
基础属性
type AIFieldConfig struct {
FieldKey string // 字段标识(编程用)
FieldName string // 字段显示名称(用户可见)
FieldType string // 字段类型
IsRequired bool // 是否必填
DefaultValue interface{} // 默认值
Validation AIFieldValidation // 验证规则
Description string // 字段描述
Extra map[string]interface{} // 额外配置(扩展点)
}支持的6种字段类型
string- 字符串int- 整数float- 浮点数bool- 布尔值array- 数组object- 对象
验证规则(AIFieldValidation)
type AIFieldValidation struct {
MinLength int // 最小长度(字符串/数组)
MaxLength int // 最大长度(字符串/数组)
Pattern string // 正则表达式(字符串)
Options []string // 枚举选项(字符串)
Min *float64 // 数值最小值
Max *float64 // 数值最大值
}扩展配置(Extra字段)
Extra 是一个 map[string]interface{},支持任意自定义配置,常见用途:
- 预览控制:
preview_visible: true - 数组结构定义:
item_schema,item_type - UI展示配置:
display_type,unit,icon,display_section - 数量控制:
expected_count,min_count,max_count
其他相关模型
AICallLog - AI调用日志
- 记录每次AI调用的完整信息
- 包含输入、输出、耗时、Token消耗等
AIProvider - AI服务提供商
- 配置OpenAI或其他AI服务的连接信息
- 支持多个提供商配置
AIModel - AI模型配置
- 定义可用的AI模型
- 关联到具体的提供商
2. 核心服务逻辑(service/ai_function.go)
缓存机制
设计目标: 减少数据库查询,提高性能
实现方式:
var (
aiConfigCache = make(map[string]*model.AIFunction)
aiConfigCacheMux sync.RWMutex
aiConfigCacheTime = time.Now()
cacheExpireDuration = 10 * time.Minute
)缓存策略:
- 首次访问: 从数据库加载 → 存入缓存
- 后续访问:
- 缓存未过期(10分钟内)→ 直接返回缓存
- 缓存已过期 → 重新从数据库加载
- 并发安全: 使用读写锁(RWMutex)保护缓存访问
手动刷新:
RefreshAIConfigCache(): 清空缓存,强制重新加载
二、CallAI完整执行流程
流程概览
用户请求
↓
[阶段1: 准备阶段]
→ 获取AI功能配置(缓存/数据库)
→ 验证输入数据
→ 构建AI提示词
↓
[阶段2: AI调用阶段]
→ 获取模型配置
→ 构建OpenAI请求
→ 调用OpenAI API
↓
[阶段3: 响应处理阶段]
→ 解析AI原始响应
→ 处理输出数据(根据output_format)
→ 特殊功能增强(如期刊数据增强)
→ 记录调用日志
→ 创建用户历史记录
↓
[阶段4: 预览模式处理]
→ 判断用户权限状态
→ 生成预览数据(字段过滤)
→ 计算付费控制字段
↓
返回响应给用户阶段1: 准备阶段(第138-157行)
1.1 获取AI功能配置
function := GetAIFunctionByKey(req.FunctionKey)
expect.PBMC(function == nil, code.PARAMETER_ERROR,
fmt.Sprintf("AI功能 '%s' 不存在或已禁用", req.FunctionKey))- 通过
function_key查询配置 - 先查缓存,未命中则查数据库
- 配置不存在或已禁用时返回错误
1.2 验证输入数据
err = validateAIInputData(req.InputData, function.InputFields)根据 input_fields 配置进行三层验证:
- 必填检查: 检查必填字段是否存在
- 类型验证: 验证字段类型是否匹配
- 值验证: 验证字段值是否符合规则
详细验证逻辑见三、输入验证体系
1.3 构建AI提示词
prompt, err := buildAIPrompt(function.PromptTemplate, req.InputData)处理步骤:
- 使用模板引擎替换占位符
{{subject}}→ 用户输入的专业名称{{title}}→ 用户输入的选题名称
- 清理输入,防止注入攻击
- 调用
utils.SanitizeInput()
- 调用
示例转换:
模板: "我的专业是{{subject}},请评估选题:{{title}}"
输入: {"subject": "工商管理", "title": "数字化转型研究"}
输出: "我的专业是工商管理,请评估选题:数字化转型研究"阶段2: AI调用阶段(第156-178行)
2.1 获取模型配置
modelConfig, err := getAIEffectiveModelConfig(function)配置合并逻辑:
默认配置:
- Model: "gpt-3.5-turbo"
- Temperature: 0.7
- MaxTokens: 2000
- TopP: 1.0
- Timeout: 60
- RetryTimes: 3
↓ 合并 function.ModelConfig
最终配置:
- 如果 function.ModelConfig.Model != "",使用自定义值
- 如果 function.ModelConfig.Temperature > 0,使用自定义值
- 其他参数同理2.2 构建OpenAI请求
openaiReq := &utils.OpenAIRequest{
Model: modelConfig.Model,
Temperature: modelConfig.Temperature,
MaxTokens: modelConfig.MaxTokens,
TopP: modelConfig.TopP,
FrequencyPenalty: modelConfig.FrequencyPenalty,
PresencePenalty: modelConfig.PresencePenalty,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: prompt,
},
},
}2.3 调用OpenAI API
openaiResp, err = utils.CallOpenAI(ctx, openaiReq)
callDuration := int(time.Since(startTime).Milliseconds())- 发送HTTP请求到OpenAI API
- 记录调用耗时
- 处理超时、重试等
阶段3: 响应处理阶段(第194-302行)
3.1 处理调用失败场景
if err != nil || openaiResp.Error != nil {
errorMsg := ""
if err != nil {
errorMsg = err.Error()
} else if openaiResp.Error != nil {
errorMsg = openaiResp.Error.Message
}
resp.ErrorMessage = errorMsg
callLog.Status = "error"
callLog.ErrorMessage = errorMsg
}- 记录错误信息到日志
- 返回错误响应给用户
3.2 处理调用成功场景
步骤1: 提取原始响应
rawResponse := ""
if len(openaiResp.Choices) > 0 {
rawResponse = openaiResp.Choices[0].Message.Content
}步骤2: 处理输出数据
outputData, err := processAIOutputData(rawResponse,
function.OutputFields, function.OutputFormat)根据 output_format 进行不同处理:
| 格式 | 处理方式 |
|---|---|
json_object | 解析为JSON对象 map[string]interface{} |
json_array | 解析为JSON数组 []interface{} |
plain_text | 直接返回原始文本 string |
markdown | 直接返回Markdown文本 string |
步骤3: 特殊功能增强(针对智能投稿期刊)
if req.FunctionKey == "ai_journal_submission" {
enrichedData, enrichErr := enrichJournalDataWithDetails(outputData)
if enrichErr != nil {
// 处理错误
} else {
outputData = enrichedData
}
}步骤4: 记录调用日志
callLog.ResponseRaw = rawResponse
callLog.TokensUsed = openaiResp.Usage.TotalTokens
callLog.Status = "success"
callLog.InputData = json.Marshal(req.InputData)
callLog.OutputData = json.Marshal(outputData)
err = callLog.Create()保存到 ai_call_logs 表,包含:
- 输入数据
- 输出数据
- 提示词
- 原始响应
- Token消耗
- 调用耗时
- 状态
步骤5: 创建用户历史记录
history, historyErr := CreateAIFunctionUsage(
req.UserInfo.Uuid,
req.FunctionKey,
req.InputData,
outputData,
callLog.ID,
req.UserInfo,
req.OrgInfo
)保存到 ai_function_usage 表,用于:
- 用户历史记录查询
- 预览/完整状态管理
- 付费控制
- 去重检查
阶段4: 预览模式处理(第284-298行)
4.1 判断预览状态
resp.IsPreview = history.ViewStatus == conf.ViewStatusPreviewViewStatus取值:
ViewStatusPreview: 预览模式(需要付费或升级会员才能查看完整内容)ViewStatusFull: 完整模式(可以查看所有内容)
判断逻辑(在CreateAIFunctionUsage中):
- 如果功能完全免费(
Price = 0)→ Full - 如果用户是会员且会员免费(
VipPrice = 0)→ Full - 其他情况 → Preview
4.2 生成预览数据
if resp.IsPreview {
previewData := getPreviewData(
history.OutputData,
history.FunctionKey,
history.PreviewCount
)
var previewInterface interface{}
err := json.Unmarshal(previewData, &previewInterface)
if err == nil {
resp.OutputData = previewInterface
}
}getPreviewData逻辑:
- 获取AI功能配置(读取
output_fields) - 遍历配置字段,过滤出
extra.preview_visible = true的字段 - 构建只包含预览字段的数据对象
- 如果字段是数组,可能截取前
previewCount条
详见service/ai_function_usage.go:375-461
4.3 计算付费控制字段
fillPaymentFields(resp, req.FunctionKey, req.UserInfo, req.OrgInfo)计算的字段:
show_payment_buttons: 是否显示付费按钮payment_price: 需要支付的金额(根据会员状态可能是原价或折扣价)show_free_trial_tip: 是否显示免费试用提示remaining_free_times: 剩余免费次数member_tip_text: 会员提示文本(如"会员免费"、"会员享折扣")
四种场景:
| 场景 | Price | VipPrice | 会员状态 | 显示按钮 | 价格 | 提示 |
|---|---|---|---|---|---|---|
| 完全免费 | 0 | 0 | - | ❌ | 0 | - |
| 会员免费 | >0 | 0 | 是会员 | ❌ | 0 | 会员免费 |
| 会员免费 | >0 | 0 | 非会员 | ✅ | Price | 会员免费 |
| 会员折扣 | >0 | >0 | 是会员 | ✅ | VipPrice | 会员享折扣 |
| 会员折扣 | >0 | >0 | 非会员 | ✅ | Price | 会员享折扣 |
| 无优惠 | >0 | >0(=Price) | - | ✅ | Price | - |
三、输入验证体系
validateAIInputData 完整逻辑
func validateAIInputData(inputData map[string]interface{},
fields []model.AIFieldConfig) error {
for _, field := range fields {
value, exists := inputData[field.FieldKey]
// 第一层: 必填检查
if field.IsRequired && (!exists || tool.IsEmpty(value)) {
return fmt.Errorf("必填字段 '%s' 缺失或为空", field.FieldName)
}
if !exists {
continue // 非必填字段不存在,跳过后续验证
}
// 第二层: 类型验证
if err := validateAIFieldType(value, field); err != nil {
return fmt.Errorf("字段 '%s' %v", field.FieldName, err)
}
// 第三层: 值验证
if err := validateAIFieldValue(value, field); err != nil {
return fmt.Errorf("字段 '%s' %v", field.FieldName, err)
}
}
return nil
}第一层: 必填检查
规则:
如果 field.IsRequired = true 且 (字段不存在 OR 值为空)
→ 返回错误空值判断:
nil- 空字符串
"" - 空数组
[] - 空对象
{}
第二层: 类型验证(validateAIFieldType)
string类型
if _, ok := value.(string); !ok {
return fmt.Errorf("类型错误,期望字符串类型")
}int类型
switch v := value.(type) {
case int, int8, int16, int32, int64:
// 合法的整数类型
case float64:
// JSON反序列化数字默认为float64,需检查是否为整数
if v != float64(int64(v)) {
return fmt.Errorf("类型错误,期望整数类型")
}
default:
return fmt.Errorf("类型错误,期望整数类型")
}注意: Go的JSON反序列化会将所有数字解析为 float64,需要特殊处理。
float类型
switch value.(type) {
case float32, float64, int, int8, int16, int32, int64:
// 合法的数字类型
default:
return fmt.Errorf("类型错误,期望数字类型")
}bool类型
if _, ok := value.(bool); !ok {
return fmt.Errorf("类型错误,期望布尔类型")
}array类型
switch value.(type) {
case []interface{}, []string, []int, []float64:
// 合法的数组类型
default:
return fmt.Errorf("类型错误,期望数组类型")
}object类型
if _, ok := value.(map[string]interface{}); !ok {
return fmt.Errorf("类型错误,期望对象类型")
}第三层: 值验证(validateAIFieldValue)
字符串验证
长度验证:
if field.FieldType == "string" {
if str, ok := value.(string); ok {
strLen := len([]rune(str)) // 使用rune计算,支持中文
if field.Validation.MinLength > 0 && strLen < field.Validation.MinLength {
return fmt.Errorf("长度不能少于%d个字符", field.Validation.MinLength)
}
if field.Validation.MaxLength > 0 && strLen > field.Validation.MaxLength {
return fmt.Errorf("长度不能超过%d个字符", field.Validation.MaxLength)
}
}
}关键点: 使用 []rune(str) 而不是 len(str),正确计算中文字符数。
正则表达式验证:
if field.Validation.Pattern != "" && field.FieldType == "string" {
if str, ok := value.(string); ok {
matched, err := regexp.MatchString(field.Validation.Pattern, str)
if err != nil {
return fmt.Errorf("正则表达式验证失败: %v", err)
}
if !matched {
return fmt.Errorf("格式不符合要求")
}
}
}枚举值验证:
if len(field.Validation.Options) > 0 && field.FieldType == "string" {
if str, ok := value.(string); ok {
found := false
for _, option := range field.Validation.Options {
if str == option {
found = true
break
}
}
if !found {
return fmt.Errorf("值必须是以下选项之一: %v", field.Validation.Options)
}
}
}数值验证
范围验证:
if field.FieldType == "int" || field.FieldType == "float" {
var numValue float64
// 类型转换为float64
switch v := value.(type) {
case int:
numValue = float64(v)
case int8, int16, int32, int64:
numValue = float64(v)
case float32, float64:
numValue = float64(v)
default:
return fmt.Errorf("无法验证数值范围")
}
// 验证范围
if field.Validation.Min != nil && numValue < *field.Validation.Min {
return fmt.Errorf("数值不能小于%.2f", *field.Validation.Min)
}
if field.Validation.Max != nil && numValue > *field.Validation.Max {
return fmt.Errorf("数值不能大于%.2f", *field.Validation.Max)
}
}数组验证
元素数量验证:
if field.FieldType == "array" {
var arrayLen int
switch arr := value.(type) {
case []interface{}:
arrayLen = len(arr)
case []string:
arrayLen = len(arr)
case []int:
arrayLen = len(arr)
case []float64:
arrayLen = len(arr)
default:
return fmt.Errorf("无法验证数组长度")
}
if field.Validation.MinLength > 0 && arrayLen < field.Validation.MinLength {
return fmt.Errorf("数组长度不能少于%d个元素", field.Validation.MinLength)
}
if field.Validation.MaxLength > 0 && arrayLen > field.Validation.MaxLength {
return fmt.Errorf("数组长度不能超过%d个元素", field.Validation.MaxLength)
}
}验证示例
配置:
{
"field_key": "subject",
"field_name": "专业名称",
"field_type": "string",
"is_required": true,
"validation": {
"min_length": 2,
"max_length": 100,
"pattern": "^[\\u4e00-\\u9fa5a-zA-Z]+$"
}
}验证过程:
| 输入 | 验证结果 |
|---|---|
| 未提供字段 | ❌ 必填字段 '专业名称' 缺失或为空 |
"" | ❌ 必填字段 '专业名称' 缺失或为空 |
"计" | ❌ 字段 '专业名称' 长度不能少于2个字符 |
123 | ❌ 字段 '专业名称' 类型错误,期望字符串类型 |
"计算机123" | ❌ 字段 '专业名称' 格式不符合要求 |
"计算机科学" | ✅ 验证通过 |
"Computer Science" | ✅ 验证通过 |
四、输出字段配置的灵活性
Extra 字段的多种用途
extra 是一个 map[string]interface{},提供了强大的扩展能力。
1. 预览控制
用途: 标记字段在预览模式下是否可见
配置示例:
{
"field_key": "overall_score",
"field_name": "综合评分",
"field_type": "int",
"extra": {
"preview_visible": true
}
}作用机制:
getPreviewData函数会检查此标记- 只有
preview_visible: true的字段才会包含在预览数据中 - 未标记的字段默认不在预览中显示
2. 数组元素结构定义
用途: 定义数组字段中每个元素的结构
配置示例:
{
"field_key": "topics",
"field_name": "生成的选题列表",
"field_type": "array",
"extra": {
"item_type": "object",
"item_schema": {
"title": "选题名称",
"reason": "推荐理由",
"innovation": "创新性说明",
"logic_structure": "逻辑结构"
}
}
}作用:
- 文档化数组元素的结构
- 前端可以根据
item_schema渲染UI - 提示AI按照此结构生成数据
3. UI展示配置
用途: 指导前端如何渲染字段
配置示例:
{
"field_key": "overall_score",
"field_name": "综合评分",
"field_type": "int",
"extra": {
"display_type": "circular_progress", // 显示为圆形进度环
"unit": "分", // 单位
"display_section": "综合评价", // 所属分组
"highlight_color": "yellow", // 高亮颜色
"icon": "💡" // 图标
}
}display_type 常见值:
circular_progress: 圆形进度环(常用于总分)progress_bar: 横向进度条(常用于分项评分)text: 普通文本badge: 徽章样式list: 列表展示
前端渲染示例:
// 根据 display_type 选择组件
switch (field.extra.display_type) {
case 'circular_progress':
return <CircularProgress value={data.overall_score} unit={field.extra.unit} />
case 'progress_bar':
return <ProgressBar value={data.feasibility_score} />
default:
return <Text>{data[field.field_key]}</Text>
}4. 数量期望配置
用途: 提示AI生成特定数量的结果
配置示例:
{
"field_key": "risks_and_challenges",
"field_name": "风险与挑战",
"field_type": "array",
"extra": {
"expected_count": 4, // 期望生成4个
"min_count": 3, // 至少3个
"max_count": 6 // 最多6个
}
}在提示词中的应用:
请列出研究过程中可能遇到的主要风险和挑战(3-6个,建议4个)5. 字段关系定义
用途: 定义嵌套对象的结构
配置示例:
{
"field_key": "current_status",
"field_name": "现状概况",
"field_type": "object",
"extra": {
"item_schema": {
"type": "object",
"properties": {
"introduction": {
"type": "string",
"description": "研究现状的概括性导语"
},
"trends": {
"type": "array",
"description": "主要研究趋势列表",
"items": {
"type": "string"
}
}
}
}
}
}数据结构:
{
"current_status": {
"introduction": "该领域研究现状的概括...",
"trends": [
"研究趋势1",
"研究趋势2",
"研究趋势3"
]
}
}6. 枚举值约束
用途: 限制数组元素的title字段只能是特定值
配置示例:
{
"field_key": "feasibility_analysis",
"field_name": "选题可行性分析",
"field_type": "array",
"extra": {
"item_schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"enum": ["优势", "挑战"],
"description": "分析维度标题:优势或挑战"
},
"content": {
"type": "string",
"description": "该维度的详细分析内容"
}
}
},
"expected_count": 2
}
}提示AI:
请从"优势"和"挑战"两个维度分析选题可行性,每个维度包含:
- title: 必须是"优势"或"挑战"
- content: 详细分析内容Extra配置最佳实践
✅ 推荐做法
文档化所有extra字段
json// 在配置中添加注释说明 "extra": { "preview_visible": true, // 预览模式可见 "display_type": "progress_bar", // UI组件类型 "unit": "分" // 显示单位 }使用统一的命名规范
- 使用 snake_case:
preview_visible,item_schema - 避免使用驼峰:
previewVisible
- 使用 snake_case:
提供合理的默认值
go// 在代码中处理 extra 字段时提供默认值 previewVisible := false if extra != nil { if v, ok := extra["preview_visible"].(bool); ok { previewVisible = v } }
❌ 避免的做法
不要在extra中存储业务数据
json// ❌ 错误 "extra": { "user_score": 85 // 业务数据应该在 default_value 或数据表中 }不要使用过于复杂的嵌套
json// ❌ 过于复杂 "extra": { "config": { "display": { "mobile": { "type": "compact" } } } } // ✅ 扁平化 "extra": { "mobile_display_type": "compact" }
五、典型案例流程
案例1: 智能选题测评(ai_topic_evaluation)
功能概述
对学术选题进行全面评估,包括可行性、创新性、价值性等多维度分析。
配置结构
输入字段(input_fields):
[
{
"field_key": "subject",
"field_name": "专业名称",
"field_type": "string",
"is_required": true,
"validation": {
"min_length": 2,
"max_length": 100
}
},
{
"field_key": "title",
"field_name": "选题名称",
"field_type": "string",
"is_required": true,
"validation": {
"min_length": 5,
"max_length": 500
}
}
]输出字段(output_fields): 12个字段
预览可见字段(7个):
title- 选题名称(回显)overall_score- 综合评分feasibility_score- 可行性得分innovation_score- 创新性得分value_score- 价值性得分overall_evaluation- 综合评价feasibility_analysis- 选题可行性分析
完整版字段(5个,预览不可见): 8. innovation_analysis - 选题创新性分析 9. value_analysis - 选题价值性分析 10. background_significance - 背景与意义 11. risks_and_challenges - 风险与挑战 12. current_status - 现状概况
执行流程详解
步骤1: 用户提交请求
{
"function_key": "ai_topic_evaluation",
"input_data": {
"subject": "工商管理",
"title": "数字化转型对中小企业创新能力的影响研究"
}
}步骤2: 验证输入
- 检查
subject和title都存在且为字符串 - 验证
subject长度在 2-100 之间 - 验证
title长度在 5-500 之间
步骤3: 构建提示词
我是一名在读的博士生,我的专业是工商管理,我正在开展学术期刊论文选题工作,
我初步拟定的选题名称是:数字化转型对中小企业创新能力的影响研究。
请你充当我的学术指导教师,从以下几个方面对该选题进行全面评估:
...(详细的评估要求)
请严格按照以下 JSON 格式输出:
{
"title": "选题名称",
"overall_score": 85,
"feasibility_score": 78,
...
}步骤4: 调用GPT-4o
- Model:
gpt-4o - Temperature:
0.7 - MaxTokens:
6000
步骤5: 解析AI响应 AI返回完整的JSON对象,包含所有12个字段。
步骤6: 判断用户权限
场景A: 会员用户或免费功能
// CreateAIFunctionUsage 中的判断逻辑
if functionConfig.Price == 0 ||
(functionConfig.VipPrice == 0 && isVip) {
viewStatus = conf.ViewStatusFull
}返回完整数据(12个字段):
{
"id": 123,
"is_preview": false,
"view_status": 2,
"output_data": {
"title": "数字化转型对中小企业创新能力的影响研究",
"overall_score": 85,
"feasibility_score": 78,
"innovation_score": 90,
"value_score": 88,
"overall_evaluation": "该选题具有较强的学术价值...",
"feasibility_analysis": [...],
"innovation_analysis": [...], // ✅ 包含
"value_analysis": [...], // ✅ 包含
"background_significance": "...", // ✅ 包含
"risks_and_challenges": [...], // ✅ 包含
"current_status": {...} // ✅ 包含
}
}场景B: 普通用户且需付费
viewStatus = conf.ViewStatusPreview步骤7: 生成预览数据
previewData := getPreviewData(history.OutputData,
history.FunctionKey, history.PreviewCount)getPreviewData执行过程:
- 加载
ai_topic_evaluation的output_fields配置 - 遍历12个字段配置
- 过滤出
extra.preview_visible = true的字段 - 从完整数据中提取这7个字段
- 如果字段是数组(如
feasibility_analysis),检查是否需要截取
返回预览数据(7个字段):
{
"id": 123,
"is_preview": true,
"view_status": 1,
"output_data": {
"title": "数字化转型对中小企业创新能力的影响研究",
"overall_score": 85,
"feasibility_score": 78,
"innovation_score": 90,
"value_score": 88,
"overall_evaluation": "该选题具有较强的学术价值...",
"feasibility_analysis": [...]
// ❌ innovation_analysis 不包含
// ❌ value_analysis 不包含
// ❌ background_significance 不包含
// ❌ risks_and_challenges 不包含
// ❌ current_status 不包含
},
"show_payment_buttons": true,
"payment_price": 500,
"remaining_free_times": 2,
"member_tip_text": "会员用户免费查看"
}步骤8: 计算付费信息
fillPaymentFields(resp, req.FunctionKey, req.UserInfo, req.OrgInfo)假设配置为:
Price: 500(普通用户价格)VipPrice: 0(会员免费)FreeTimes: 3(免费次数)
对于普通用户:
show_payment_buttons:truepayment_price:500remaining_free_times:2(已使用1次)member_tip_text:"会员用户免费查看"
案例2: 智能投稿期刊(ai_journal_submission)
功能概述
根据研究内容和方向,推荐适合投稿的学术期刊,并提供详细的期刊信息。
特殊处理逻辑
此功能与其他AI功能的不同之处在于需要数据增强:AI只返回期刊名称和评分,需要通过RPC服务获取期刊的完整详情。
执行流程
步骤1-5: 与普通功能相同
步骤6: AI返回原始数据
{
"journals": [
{
"journal_name": "管理世界",
"match_score": 95,
"difficulty_score": 85,
"recommend_score": 92,
"recommend_reason": "《管理世界》是中国管理学顶级期刊...",
"submission_advice": "建议突出研究的理论贡献..."
},
{
"journal_name": "中国工业经济",
"match_score": 93,
"difficulty_score": 80,
"recommend_score": 90,
"recommend_reason": "...",
"submission_advice": "..."
}
// ... 更多期刊
]
}步骤7: 数据增强(enrichJournalDataWithDetails)
if req.FunctionKey == "ai_journal_submission" {
enrichedData, enrichErr := enrichJournalDataWithDetails(outputData)
if enrichErr != nil {
// 增强失败,返回错误
expect.PBM(true, callLog.ErrorMessage)
} else {
outputData = enrichedData
}
}enrichJournalDataWithDetails 执行过程:
7.1 解析输入数据
var dataArray []interface{}
json.Unmarshal(outputData, &dataArray)7.2 判断数据类型 检查第一条数据:
- 如果包含
journal_id→ 按ID查询(用于历史记录场景) - 如果只有
journal_name→ 按名称查询(用于AI调用场景)
7.3 提取查询参数
names := []string{"管理世界", "中国工业经济", ...}7.4 调用RPC服务
journalDetails := journal.JournalCnListByNames(names)返回期刊详情:
[]journals.JournalInfo{
{
JournalId: 123,
CNJournalName: "管理世界",
ImpactFactor: 8.5,
Partition: "T1",
CNIssn: "1002-5502",
Host: "中国社会科学院",
SubmissionUrl: "http://...",
// ... 更多字段
},
...
}7.5 构建期刊映射
nameToDetail := map[string]journals.JournalInfo{
"管理世界": {...},
"中国工业经济": {...},
}7.6 合并AI评分与期刊详情
for _, item := range dataArray {
name := item["journal_name"]
detail := nameToDetail[name]
merged := map[string]interface{}{
// AI评分数据
"journal_name": item["journal_name"],
"journal_id": detail.JournalId,
"match_score": item["match_score"],
"difficulty_score": item["difficulty_score"],
"recommend_score": item["recommend_score"],
"recommend_reason": item["recommend_reason"],
"submission_advice": item["submission_advice"],
// 完整期刊详情
"journal_detail": detail,
}
enrichedResult = append(enrichedResult, merged)
}7.7 按推荐度排序
sort.Slice(enrichedResult, func(i, j int) bool {
scoreI := enrichedResult[i]["recommend_score"].(float64)
scoreJ := enrichedResult[j]["recommend_score"].(float64)
return scoreI > scoreJ
})7.8 限制返回数量
if len(enrichedResult) > 10 {
enrichedResult = enrichedResult[:10]
}步骤8: 返回增强后的数据
{
"journals": [
{
"journal_name": "管理世界",
"journal_id": 123,
"match_score": 95,
"difficulty_score": 85,
"recommend_score": 92,
"recommend_reason": "《管理世界》是中国管理学顶级期刊...",
"submission_advice": "建议突出研究的理论贡献...",
"journal_detail": {
"journal_id": 123,
"cn_journal_name": "管理世界",
"impact_factor": 8.5,
"partition": "T1",
"cn_issn": "1002-5502",
"host": "中国社会科学院",
"submission_url": "http://...",
"review_cycle": "3-6个月",
"acceptance_rate": "8%",
// ... 完整期刊信息
}
},
// ... 最多10条
]
}数据增强的优势
- 分离关注点: AI专注于推荐和评分,期刊详情由专业数据库提供
- 数据一致性: 期刊详情实时更新,无需AI重新训练
- 降低Token消耗: AI不需要生成详细的期刊信息
- 提高准确性: 期刊数据来自权威数据库,更准确可靠
案例3: 复刻学术选题(ai_topic_replication)
功能概述
根据用户提供的参考文献,生成模仿其理论/方法的学术选题。
配置结构
输入字段:
[
{
"field_key": "subject",
"field_name": "专业",
"field_type": "string",
"is_required": true
},
{
"field_key": "reference",
"field_name": "参考文献",
"field_type": "string",
"is_required": true,
"validation": {
"min_length": 50
}
}
]输出字段: 数组格式
{
"field_key": "topics",
"field_name": "生成的复刻选题列表",
"field_type": "array",
"extra": {
"item_type": "object",
"item_schema": {
"title": "选题名称",
"innovation": "创新性与新颖性",
"theory_method": "模仿的理论/方法",
"logic_structure": "逻辑结构"
},
"expected_count": 5,
"min_count": 5
}
}AI输出示例
{
"topics": [
{
"title": "数字化转型对中小企业创新能力的影响研究",
"innovation": "将数字化转型理论应用于中国中小企业情境,填补该领域实证研究空白",
"theory_method": "采用参考文献中的双差分模型(DID)和中介效应分析框架,模仿其实证研究方法论",
"logic_structure": "现象观察→理论推导→假设提出→实证检验→机制分析→稳健性检验,完整复刻原文逻辑链条"
},
{
"title": "供应链数字化对企业绩效的影响机制研究",
"innovation": "从供应链视角拓展数字化研究边界,结合中国制造业转型升级背景",
"theory_method": "借鉴参考文献的资源基础理论框架和PSM-DID方法,构建类似因果推断模型",
"logic_structure": "问题提出→文献综述→理论框架→研究假设→数据与方法→实证结果→结论与建议"
},
// ... 至少5个选题
]
}提示词构建关键点
item_schema的应用:
请生成至少5个复刻学术选题,每个选题必须包含以下4个部分:
1. title: 选题名称(清晰、具体、学术性强)
2. innovation: 创新性与新颖性说明(如何在模仿中创新)
3. theory_method: 模仿的理论/方法(具体说明复刻了参考文献的哪些理论或方法)
4. logic_structure: 逻辑结构(论文的章节结构或研究流程)
请严格按照以下JSON格式输出:
{
"topics": [
{
"title": "...",
"innovation": "...",
"theory_method": "...",
"logic_structure": "..."
}
]
}数组字段的处理
前端渲染:
// 遍历topics数组
data.topics.map((topic, index) => (
<div key={index} className="topic-card">
<h3>{index + 1}. {topic.title}</h3>
<div className="topic-section">
<h4>创新性与新颖性</h4>
<p>{topic.innovation}</p>
</div>
<div className="topic-section">
<h4>模仿的理论/方法</h4>
<p>{topic.theory_method}</p>
</div>
<div className="topic-section">
<h4>逻辑结构</h4>
<p>{topic.logic_structure}</p>
</div>
</div>
))六、配置系统的设计优势
1. 配置驱动,灵活扩展
无需修改代码:
- 新增AI功能只需在数据库插入一条配置记录
- 修改功能只需更新数据库记录
- 启用/禁用功能只需修改
status字段
示例:
-- 新增一个AI功能
INSERT INTO ai_functions (
function_key, function_name, description,
prompt_template, input_fields, output_fields,
output_format, model_config, status, sort
) VALUES (
'ai_paper_outline',
'智能论文大纲',
'根据选题生成论文大纲',
'...', -- 提示词模板
'[...]'::jsonb, -- 输入字段配置
'[...]'::jsonb, -- 输出字段配置
'json_object',
'{...}'::jsonb, -- 模型配置
1,
20
);扩展性:
- 通过
extra字段添加任意自定义配置 - 不需要修改数据库结构
- 向后兼容,旧功能不受影响
2. 类型安全的验证体系
编译时类型检查:
type AIFieldConfig struct {
FieldKey string // ✅ 编译时检查
FieldName string // ✅ 编译时检查
FieldType string // ✅ 编译时检查
IsRequired bool // ✅ 编译时检查
Validation AIFieldValidation // ✅ 编译时检查
Extra map[string]interface{} // ⚠️ 运行时检查
}运行时验证:
- 输入数据类型验证
- 输入数据值验证(长度、范围、格式)
- 详细的错误消息
防御性编程:
// 安全地访问 extra 字段
previewVisible := false
if extra != nil {
if v, ok := extra["preview_visible"].(bool); ok {
previewVisible = v
}
}3. 分离的模板系统
两级模板:
| 模板 | 用途 | 受众 | 内容 |
|---|---|---|---|
prompt_template | 发送给AI | AI模型 | 完整详细,包含指令、格式、示例 |
user_prompt_template | 展示给用户 | 终端用户 | 简洁易懂,引导用户输入 |
示例对比:
prompt_template (约3000字):
我是一名在读的博士生,我的专业是{{subject}},我正在开展学术期刊论文选题工作,
我初步拟定的选题名称是:{{title}}。
请你充当我的学术指导教师,从以下几个方面对该选题进行全面评估:
**评分维度(百分制):**
1. 综合评分:对选题的整体学术价值进行综合评价
2. 可行性:评估研究的可实施性(数据获取、研究方法、时间安排等)
... (详细的评估要求和格式说明)user_prompt_template (约30字):
我的专业是{{subject}},请为我评估以下选题的学术价值与研究潜力:{{title}}优势:
- 用户界面简洁友好
- AI收到完整详细的指令
- 两者独立修改,互不影响
4. 多层输出控制
三层控制机制:
第一层: output_format 控制整体格式
switch outputFormat {
case "json_array":
return []interface{}{...}
case "json_object":
return map[string]interface{}{...}
case "plain_text":
return "..."
case "markdown":
return "# ..."
}第二层: output_fields 定义字段结构
[
{
"field_key": "title",
"field_type": "string"
},
{
"field_key": "topics",
"field_type": "array"
}
]第三层: extra.preview_visible 控制预览字段
{
"field_key": "overall_score",
"extra": {
"preview_visible": true
}
}协同工作:
完整数据 (12个字段)
↓
[output_fields 定义结构]
↓
预览模式判断
↓
[extra.preview_visible 过滤]
↓
预览数据 (7个字段)5. 缓存优化性能
缓存策略:
- 缓存时长: 10分钟
- 缓存键: function_key
- 并发安全: sync.RWMutex
性能对比:
| 场景 | 无缓存 | 有缓存 |
|---|---|---|
| 首次请求 | 查询数据库 (~5ms) | 查询数据库 (~5ms) |
| 10分钟内重复请求 | 查询数据库 (~5ms) | 读取缓存 (~0.01ms) |
| 高并发场景 (100 QPS) | 500ms总耗时 | 1ms总耗时 |
缓存命中率估算:
- 假设每个功能平均每分钟被调用10次
- 10分钟内缓存命中率: 99.9%
- 极大减少数据库压力
缓存刷新:
// 手动刷新缓存(管理后台调用)
service.RefreshAIConfigCache()
// 或者修改配置后自动刷新
func UpdateAIFunction(function *model.AIFunction) error {
err := function.Update(nil)
if err == nil {
// 清理缓存,下次请求时重新加载
service.RefreshAIConfigCache()
}
return err
}6. 完整的日志和历史
两层日志系统:
ai_call_logs (调用日志):
- 记录每次AI调用的技术细节
- 用于调试、监控、计费
- 包含完整的请求和响应
ai_function_usage (用户历史):
- 记录用户的使用记录
- 用于历史查询、预览管理、去重检查
- 包含预览状态和付费信息
日志用途:
| 用途 | ai_call_logs | ai_function_usage |
|---|---|---|
| 技术调试 | ✅ | ❌ |
| Token计费 | ✅ | ❌ |
| 用户历史 | ❌ | ✅ |
| 预览管理 | ❌ | ✅ |
| 去重检查 | ❌ | ✅ |
| 性能监控 | ✅ | ❌ |
查询示例:
技术人员查询某功能的平均Token消耗:
SELECT
function_key,
AVG(tokens_used) as avg_tokens,
AVG(call_duration) as avg_duration_ms
FROM ai_call_logs
WHERE function_key = 'ai_topic_evaluation'
AND status = 'success'
AND created_at >= NOW() - INTERVAL '7 days'
GROUP BY function_key;用户查询自己的使用历史:
SELECT
id,
function_key,
created_at,
view_status
FROM ai_function_usage
WHERE uuid = 'user-uuid-xxx'
ORDER BY created_at DESC
LIMIT 20;七、关键设计模式
1. 策略模式(输出格式处理)
问题: 不同的AI功能需要不同的输出格式处理逻辑
解决方案: 根据 output_format 选择不同的处理策略
实现:
func processAIOutputData(rawResponse string,
outputFields []model.AIFieldConfig,
outputFormat string) (interface{}, error) {
cleanResponse := strings.TrimSpace(rawResponse)
switch outputFormat {
case "json_array":
return processJSONArray(cleanResponse)
case "json_object":
return processJSONObject(cleanResponse, outputFields)
case "plain_text":
return processPlainText(cleanResponse)
case "markdown":
return processMarkdown(cleanResponse)
default:
return processJSONObject(cleanResponse, outputFields)
}
}优势:
- 新增格式类型只需添加新的 case
- 各策略独立实现,互不影响
- 易于测试和维护
UML类图:
┌───────────────────────┐
│ AIOutputProcessor │
├───────────────────────┤
│ +Process(format) │
└───────────┬───────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌────────▼────────┐ │ ┌────────▼────────┐
│ JSONArrayHandler│ │ │JSONObjectHandler│
├─────────────────┤ │ ├─────────────────┤
│ +Process() │ │ │ +Process() │
└─────────────────┘ │ └─────────────────┘
│
┌────────────▼──────────┐
│ PlainTextHandler │
│ MarkdownHandler │
└───────────────────────┘2. 模板方法模式(CallAI流程)
问题: AI调用有标准流程,但某些功能需要特殊处理
解决方案: 定义标准流程框架,允许子步骤定制
标准流程(CallAI):
func CallAI(ctx context.Context, req *form.AICallReq) (*response.AICallResp, error) {
// 步骤1: 准备阶段(标准)
function := GetAIFunctionByKey(req.FunctionKey)
validateAIInputData(req.InputData, function.InputFields)
prompt := buildAIPrompt(function.PromptTemplate, req.InputData)
// 步骤2: 调用阶段(标准)
openaiResp := utils.CallOpenAI(ctx, openaiReq)
// 步骤3: 处理阶段(可定制)
outputData := processAIOutputData(rawResponse, function.OutputFields, function.OutputFormat)
// 步骤4: 特殊处理(扩展点)
if req.FunctionKey == "ai_journal_submission" {
outputData = enrichJournalDataWithDetails(outputData)
}
// 步骤5: 记录日志(标准)
callLog.Create()
history := CreateAIFunctionUsage(...)
return resp, nil
}扩展点:
processAIOutputData: 可根据 output_format 定制- 特殊处理: 可针对特定功能插入定制逻辑
优势:
- 大部分功能走标准流程,无需重复代码
- 特殊功能可以插入定制逻辑
- 保持代码的一致性和可维护性
UML序列图:
3. 装饰器模式(数据增强)
问题: 期刊推荐功能需要在AI数据基础上添加详情信息
解决方案: 不修改原始数据结构,装饰性地添加信息
实现:
// AI原始数据(核心对象)
type AIJournalData struct {
JournalName string
MatchScore float64
RecommendScore float64
RecommendReason string
}
// 装饰器函数
func enrichJournalDataWithDetails(aiData interface{}) (interface{}, error) {
// 1. 解析AI数据
journals := parseAIJournals(aiData)
// 2. 获取期刊详情(装饰)
details := fetchJournalDetails(journals)
// 3. 合并数据(返回装饰后的对象)
return mergeAIDataWithDetails(journals, details), nil
}
// 装饰后的数据(增强对象)
type EnrichedJournalData struct {
// AI数据(原始)
JournalName string
MatchScore float64
RecommendScore float64
RecommendReason string
// 期刊详情(装饰)
JournalDetail JournalInfo
}关键特点:
- 不修改AI返回的原始数据
- 通过包装方式添加新信息
- 原始数据和装饰数据可以独立演化
优势:
- 职责分离: AI负责推荐,数据库负责详情
- 灵活组合: 可以多次装饰(如再添加用户评论)
- 易于测试: AI数据和详情数据可以分别测试
装饰链示例:
AI原始数据
↓
enrichJournalDataWithDetails (装饰器1)
↓
enrichWithUserReviews (装饰器2,可选)
↓
enrichWithSubmissionStats (装饰器3,可选)
↓
最终数据4. 工厂模式(模型配置)
问题: 不同的AI功能可能使用不同的模型配置
解决方案: 使用工厂方法根据功能配置动态创建模型配置对象
实现:
// 抽象产品
type ModelConfig interface {
GetModel() string
GetTemperature() float32
GetMaxTokens() int
}
// 具体产品
type AIModelConfig struct {
Model string
Temperature float32
MaxTokens int
TopP float32
FrequencyPenalty float32
PresencePenalty float32
Timeout int
RetryTimes int
}
// 工厂方法
func getAIEffectiveModelConfig(function *model.AIFunction) (*model.AIModelConfig, error) {
// 创建默认配置
baseConfig := &model.AIModelConfig{
Model: "gpt-3.5-turbo",
Temperature: 0.7,
MaxTokens: 2000,
TopP: 1.0,
Timeout: 60,
RetryTimes: 3,
}
// 根据功能配置覆盖
if function.ModelConfig.Model != "" {
baseConfig.Model = function.ModelConfig.Model
}
if function.ModelConfig.Temperature > 0 {
baseConfig.Temperature = function.ModelConfig.Temperature
}
if function.ModelConfig.MaxTokens > 0 {
baseConfig.MaxTokens = function.ModelConfig.MaxTokens
}
return baseConfig, nil
}使用场景:
场景1: 使用默认配置
// function.ModelConfig 为空
config := getAIEffectiveModelConfig(function)
// config = {Model: "gpt-3.5-turbo", Temperature: 0.7, ...}场景2: 使用GPT-4
// function.ModelConfig = {Model: "gpt-4"}
config := getAIEffectiveModelConfig(function)
// config = {Model: "gpt-4", Temperature: 0.7, ...}场景3: 高温度创造性任务
// function.ModelConfig = {Model: "gpt-4", Temperature: 1.2}
config := getAIEffectiveModelConfig(function)
// config = {Model: "gpt-4", Temperature: 1.2, ...}优势:
- 统一创建逻辑,避免重复代码
- 易于添加新的配置选项
- 可以实现配置的继承和覆盖
5. 观察者模式(缓存更新)
问题: 配置更新后需要通知缓存清理
解决方案: 当配置变更时,通知所有观察者(缓存管理器)
实现思路:
// 主题(配置管理)
type AIFunctionManager struct {
observers []CacheObserver
}
func (m *AIFunctionManager) UpdateFunction(function *model.AIFunction) error {
err := function.Update(nil)
if err == nil {
// 通知所有观察者
m.notifyObservers(function.FunctionKey)
}
return err
}
// 观察者接口
type CacheObserver interface {
OnConfigChanged(functionKey string)
}
// 具体观察者(缓存管理器)
type AIConfigCacheManager struct {}
func (c *AIConfigCacheManager) OnConfigChanged(functionKey string) {
// 清理特定功能的缓存
aiConfigCacheMux.Lock()
delete(aiConfigCache, functionKey)
aiConfigCacheMux.Unlock()
}优势:
- 配置管理和缓存管理解耦
- 可以添加多个观察者(如日志记录器、指标收集器)
- 易于扩展
总结
AI功能配置系统通过以下关键设计实现了高度的灵活性和可扩展性:
- 配置驱动: 所有功能通过数据库配置,无需修改代码
- 分层验证: 三层验证体系确保数据安全和类型正确
- 模板系统: 分离的提示词模板满足不同受众需求
- 预览控制: 灵活的字段级预览机制支持付费模式
- 缓存优化: 智能缓存大幅提升性能
- 设计模式: 运用多种设计模式保持代码的清晰和可维护性
文档版本: v1.0最后更新: 2025-10-22维护者: X-Wukong研发团队
