This commit is contained in:
fuzhongyun 2025-09-11 11:32:25 +08:00
parent fdd673810f
commit 8dd60b0877
11 changed files with 355 additions and 114 deletions

View File

@ -1,5 +1,5 @@
server:
port: 8080
port: 8090
host: "0.0.0.0"
ollama:
@ -7,12 +7,28 @@ ollama:
model: "qwen3:8b"
timeout: 30s
# 模型参数
modelParam:
temperature: 0.7
max_tokens: 2000
tools:
weather:
enabled: true
mock_data: true
calculator:
enabled: true
zltxOrderDetail: # 直连天下订单详情
enabled: true
base_url: https://gateway.dev.cdlsxd.cn
biz_system: "zltx"
zltxOrderLog: # 直连天下订单日志
enabled: true
base_url: https://gateway.dev.cdlsxd.cn
biz_system: "zltx"
knowledge: # 知识库
enabled: true
base_url: http://117.175.169.61:8080
api_key: sk-EfnUANKMj3DUOiEPJZ5xS8SGMsbO6be_qYAg9uZ8T3zyoFM-
logging:
level: "info"

View File

@ -30,14 +30,19 @@ type OllamaConfig struct {
// ToolsConfig 工具配置
type ToolsConfig struct {
Weather ToolConfig `mapstructure:"weather"`
Calculator ToolConfig `mapstructure:"calculator"`
Weather ToolConfig `mapstructure:"weather"`
Calculator ToolConfig `mapstructure:"calculator"`
ZltxOrderDetail ToolConfig `mapstructure:"zltxOrderDetail"`
ZltxOrderLog ToolConfig `mapstructure:"zltxOrderLog"`
Knowledge ToolConfig `mapstructure:"knowledge"`
}
// ToolConfig 单个工具配置
type ToolConfig struct {
Enabled bool `mapstructure:"enabled"`
MockData bool `mapstructure:"mock_data"`
Enabled bool `mapstructure:"enabled"`
BaseURL string `mapstructure:"base_url"`
APIKey string `mapstructure:"api_key"`
BizSystem string `mapstructure:"biz_system"`
}
// LoggingConfig 日志配置
@ -72,4 +77,4 @@ func LoadConfig(configPath string) (*Config, error) {
}
return &config, nil
}
}

View File

@ -0,0 +1,12 @@
package constants
type Caller string
const (
CallerZltx Caller = "zltx" // 直连天下
CallerHyt Caller = "hyt" // 货易通
)
func (c Caller) String() string {
return string(c)
}

View File

@ -0,0 +1,22 @@
package constants
type KnowledgeId string
const (
KnowledgeIdZltx KnowledgeId = "kb-00000001"
)
func (k KnowledgeId) String() string {
return string(k)
}
var CallerKnowledgeIdMap = map[Caller]KnowledgeId{
CallerZltx: KnowledgeIdZltx,
}
func GetKnowledgeId(caller Caller) KnowledgeId {
if _, ok := CallerKnowledgeIdMap[caller]; !ok {
return ""
}
return CallerKnowledgeIdMap[caller]
}

View File

@ -21,14 +21,17 @@ func NewChatHandler(routerService types.RouterService) *ChatHandler {
// ChatRequest HTTP聊天请求
type ChatRequest struct {
Message string `json:"message" binding:"required" example:"北京今天天气怎么样?"`
UserInput string `json:"user_input" binding:"required" example:"考勤规则"`
Caller string `json:"caller" binding:"required" example:"zltx"`
SessionID string `json:"session_id" example:"default"`
}
// ChatResponse HTTP聊天响应
type ChatResponse struct {
Message string `json:"message" example:"北京今天天气晴朗温度15.3°C"`
ToolCalls []ToolCallResponse `json:"tool_calls,omitempty"`
Finished bool `json:"finished" example:"true"`
Status string `json:"status" example:"success"` // 处理状态
Message string `json:"message" example:""` // 响应消息
Data any `json:"data,omitempty"` // 响应数据
TaskCode string `json:"task_code,omitempty"` // 任务代码
}
// ToolCallResponse 工具调用响应
@ -45,46 +48,31 @@ type FunctionCallResponse struct {
Arguments interface{} `json:"arguments"`
}
// ErrorResponse 错误响应
type ErrorResponse struct {
Error string `json:"error" example:"Invalid request"`
Code int `json:"code" example:"400"`
Message string `json:"message" example:"请求参数错误"`
}
// Chat 处理聊天请求
// @Summary 智能聊天
// @Description 发送消息给AI助手支持工具调用
// @Tags chat
// @Accept json
// @Produce json
// @Param request body ChatRequest true "聊天请求"
// @Success 200 {object} ChatResponse "聊天响应"
// @Failure 400 {object} ErrorResponse "请求错误"
// @Failure 500 {object} ErrorResponse "服务器错误"
// @Router /api/v1/chat [post]
func (h *ChatHandler) Chat(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "Invalid request",
Code: http.StatusBadRequest,
Message: err.Error(),
c.JSON(http.StatusBadRequest, ChatResponse{
Status: "error",
Message: "请求参数错误",
})
return
}
// 转换为服务层请求
serviceReq := &types.ChatRequest{
Message: req.Message,
UserInput: req.UserInput,
Caller: req.Caller,
SessionID: req.SessionID,
ChatRequestMeta: types.ChatRequestMeta{
Authorization: c.GetHeader("Authorization"),
},
}
// 调用路由服务
response, err := h.routerService.Route(c.Request.Context(), serviceReq)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "Service error",
Code: http.StatusInternalServerError,
c.JSON(http.StatusInternalServerError, ChatResponse{
Status: "error",
Message: err.Error(),
})
return
@ -93,38 +81,10 @@ func (h *ChatHandler) Chat(c *gin.Context) {
// 转换响应格式
httpResponse := &ChatResponse{
Message: response.Message,
Finished: response.Finished,
}
// 转换工具调用
if len(response.ToolCalls) > 0 {
httpResponse.ToolCalls = make([]ToolCallResponse, len(response.ToolCalls))
for i, toolCall := range response.ToolCalls {
httpResponse.ToolCalls[i] = ToolCallResponse{
ID: toolCall.ID,
Type: toolCall.Type,
Function: FunctionCallResponse{
Name: toolCall.Function.Name,
Arguments: toolCall.Function.Arguments,
},
Result: toolCall.Result,
}
}
Status: response.Status,
Data: response.Data,
TaskCode: response.TaskCode,
}
c.JSON(http.StatusOK, httpResponse)
}
// Health 健康检查
// @Summary 健康检查
// @Description 检查服务是否正常运行
// @Tags system
// @Produce json
// @Success 200 {object} map[string]string
// @Router /health [get]
func (h *ChatHandler) Health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"service": "ai-scheduler",
})
}

View File

@ -27,9 +27,6 @@ func SetupRoutes(routerService types.RouterService) *gin.Engine {
// 创建处理器
chatHandler := NewChatHandler(routerService)
// 健康检查
r.GET("/health", chatHandler.Health)
// API路由组
v1 := r.Group("/api/v1")
{
@ -40,4 +37,4 @@ func SetupRoutes(routerService types.RouterService) *gin.Engine {
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
return r
}
}

View File

@ -1,12 +1,14 @@
package services
import (
"ai_scheduler/internal/constants"
"ai_scheduler/internal/tools"
"ai_scheduler/pkg/types"
"context"
"encoding/json"
"fmt"
"log"
"strings"
)
// RouterService 智能路由服务
@ -27,20 +29,48 @@ func NewRouterService(aiClient types.AIClient, toolManager *tools.Manager) *Rout
func (r *RouterService) Route(ctx context.Context, req *types.ChatRequest) (*types.ChatResponse, error) {
// 构建消息
messages := []types.Message{
// {
// Role: "system",
// Content: r.buildSystemPrompt(),
// },
{
Role: "system",
Content: r.buildSystemPrompt(),
Role: "assistant",
Content: r.buildIntentPrompt(req.UserInput),
},
{
Role: "user",
Content: req.Message,
Content: req.UserInput,
},
}
// 获取工具定义
toolDefinitions := r.toolManager.GetToolDefinitions()
// 第1次调用AI获取用户意图
intentResponse, err := r.aiClient.Chat(ctx, messages, nil)
if err != nil {
return nil, fmt.Errorf("AI响应失败: %w", err)
}
// 第一次调用AI获取是否需要使用工具
// 从AI响应中提取意图
intent := r.extractIntent(intentResponse)
if intent == "" {
return nil, fmt.Errorf("未识别到用户意图")
}
switch intent {
case "order_diagnosis":
// 订单诊断意图
return r.handleOrderDiagnosis(ctx, req, messages)
case "knowledge_qa":
// 知识问答意图
return r.handleKnowledgeQA(ctx, req, messages)
default:
// 未知意图
return nil, fmt.Errorf("意图识别失败,请明确您的需求呢,我可以为您")
}
// 获取工具定义
toolDefinitions := r.toolManager.GetToolDefinitions(constants.Caller(req.Caller))
// 第2次调用AI获取是否需要使用工具
response, err := r.aiClient.Chat(ctx, messages, toolDefinitions)
if err != nil {
return nil, fmt.Errorf("failed to chat with AI: %w", err)
@ -81,31 +111,99 @@ func (r *RouterService) Route(ctx context.Context, req *types.ChatRequest) (*typ
// 合并工具调用信息到最终响应
finalResponse.ToolCalls = toolResults
log.Printf("Router processed request: %s, used %d tools", req.Message, len(toolResults))
log.Printf("Router processed request: %s, used %d tools", req.UserInput, len(toolResults))
return finalResponse, nil
}
// buildSystemPrompt 构建系统提示词
func (r *RouterService) buildSystemPrompt() string {
prompt := `你是一个智能助手可以帮助用户解决各种问题你有以下工具可以使用
`
// 添加工具描述
tools := r.toolManager.GetAllTools()
for _, tool := range tools {
prompt += fmt.Sprintf("- %s: %s\n", tool.Name(), tool.Description())
}
prompt += `
请根据用户的问题判断是否需要使用工具如果需要请调用相应的工具获取信息然后基于工具返回的结果给出完整的回答
注意事项
1. 只有在确实需要获取实时信息或进行计算时才使用工具
2. 如果用户只是普通聊天不需要使用工具
3. 使用工具后请基于工具返回的结果给出自然友好的回复
4. 如果工具执行出错请告知用户并提供替代建议`
prompt := `你是一个智能路由系统,你的任务是根据用户输入判断用户的意图,并且执行对应的任务。`
return prompt
}
}
// buildIntentPrompt 构建意图识别提示词
func (r *RouterService) buildIntentPrompt(userInput string) string {
prompt := `请分析以下用户输入判断用户的意图类型
用户输入{user_input}
意图类型说明
1. order_diagnosis - 订单诊断用户想要查询诊断或了解订单相关信息
2. knowledge_qa - 知识问答用户想要进行一般性问答或获取知识信息
- 当用户意图不够清晰且不匹配 knowledge_qa 以外意图时使用knowledge_qa
- 当用户意图非常不清晰时使用 unknown
请只返回以下格式的JSON
{
"intent": "order_diagnosis" | "knowledge_qa" | "unknown",
"confidence": 0.0-1.0,
"reasoning": "判断理由"
}
`
prompt = strings.ReplaceAll(prompt, "{user_input}", userInput)
return prompt
}
// extractIntent 从AI响应中提取意图
func (r *RouterService) extractIntent(response *types.ChatResponse) string {
if response == nil || response.Message == "" {
return ""
}
// 尝试解析JSON
var intent struct {
Intent string `json:"intent"`
Confidence string `json:"confidence"`
Reasoning string `json:"reasoning"`
}
err := json.Unmarshal([]byte(response.Message), &intent)
if err != nil {
log.Printf("Failed to parse intent JSON: %v", err)
return ""
}
return intent.Intent
}
// handleOrderDiagnosis 处理订单诊断意图
func (r *RouterService) handleOrderDiagnosis(ctx context.Context, req *types.ChatRequest, messages []types.Message) (*types.ChatResponse, error) {
// 调用订单详情工具
orderDetailTool, ok := r.toolManager.GetTool("zltxOrderDetail")
if orderDetailTool == nil || !ok {
return nil, fmt.Errorf("order detail tool not found")
}
orderDetailTool.Execute(ctx, json.RawMessage{})
// 获取相关工具定义
toolDefinitions := r.toolManager.GetToolDefinitions(constants.Caller(req.Caller))
// 调用AI获取是否需要使用工具
response, err := r.aiClient.Chat(ctx, messages, toolDefinitions)
if err != nil {
return nil, fmt.Errorf("failed to chat with AI: %w", err)
}
// 如果没有工具调用,直接返回
if len(response.ToolCalls) == 0 {
return response, nil
}
// 执行工具调用
toolResults, err := r.toolManager.ExecuteToolCalls(ctx, response.ToolCalls)
if err != nil {
return nil, fmt.Errorf("failed to execute tools: %w", err)
}
return nil, nil
}
// handleKnowledgeQA 处理知识问答意图
func (r *RouterService) handleKnowledgeQA(ctx context.Context, req *types.ChatRequest, messages []types.Message) (*types.ChatResponse, error) {
return nil, nil
}

View File

@ -2,6 +2,7 @@ package tools
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/constants"
"ai_scheduler/pkg/types"
"context"
"encoding/json"
@ -21,7 +22,7 @@ func NewManager(config *config.ToolsConfig) *Manager {
// 注册天气工具
if config.Weather.Enabled {
weatherTool := NewWeatherTool(config.Weather.MockData)
weatherTool := NewWeatherTool()
m.tools[weatherTool.Name()] = weatherTool
}
@ -31,6 +32,24 @@ func NewManager(config *config.ToolsConfig) *Manager {
m.tools[calcTool.Name()] = calcTool
}
// 注册知识库工具
// if config.Knowledge.Enabled {
// knowledgeTool := NewKnowledgeTool()
// m.tools[knowledgeTool.Name()] = knowledgeTool
// }
// 注册直连天下订单详情工具
if config.ZltxOrderDetail.Enabled {
zltxOrderDetailTool := NewZltxOrderDetailTool(config.ZltxOrderDetail)
m.tools[zltxOrderDetailTool.Name()] = zltxOrderDetailTool
}
// 注册直连天下订单日志工具
// if config.ZltxOrderLog.Enabled {
// zltxOrderLogTool := NewZltxOrderLogTool(config.ZltxOrderLog)
// m.tools[zltxOrderLogTool.Name()] = zltxOrderLogTool
// }
return m
}
@ -50,11 +69,12 @@ func (m *Manager) GetAllTools() []types.Tool {
}
// GetToolDefinitions 获取所有工具定义
func (m *Manager) GetToolDefinitions() []types.ToolDefinition {
func (m *Manager) GetToolDefinitions(caller constants.Caller) []types.ToolDefinition {
definitions := make([]types.ToolDefinition, 0, len(m.tools))
for _, tool := range m.tools {
definitions = append(definitions, tool.Definition())
}
return definitions
}
@ -98,4 +118,4 @@ func (m *Manager) ExecuteToolCalls(ctx context.Context, toolCalls []types.ToolCa
}
return results, nil
}
}

View File

@ -15,10 +15,8 @@ type WeatherTool struct {
}
// NewWeatherTool 创建天气工具
func NewWeatherTool(mockData bool) *WeatherTool {
return &WeatherTool{
mockData: mockData,
}
func NewWeatherTool() *WeatherTool {
return &WeatherTool{}
}
// Name 返回工具名称
@ -137,4 +135,4 @@ func (w *WeatherTool) getMockWeather(city, unit string) *WeatherResponse {
WindSpeed: float64(rand.Intn(20)) + 1.0,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
}
}
}

View File

@ -0,0 +1,105 @@
package tools
import (
"ai_scheduler/internal/config"
"ai_scheduler/pkg/types"
"context"
"encoding/json"
"fmt"
"net/http"
)
// ZltxOrderDetailTool 直连天下订单详情工具
type ZltxOrderDetailTool struct {
config config.ToolConfig
}
// NewZltxOrderDetailTool 创建直连天下订单详情工具
func NewZltxOrderDetailTool(config config.ToolConfig) *ZltxOrderDetailTool {
return &ZltxOrderDetailTool{config: config}
}
// Name 返回工具名称
func (w *ZltxOrderDetailTool) Name() string {
return "zltxOrderDetail"
}
// Description 返回工具描述
func (w *ZltxOrderDetailTool) Description() string {
return "获取直连天下订单详情"
}
// Definition 返回工具定义
func (w *ZltxOrderDetailTool) Definition() types.ToolDefinition {
return types.ToolDefinition{
Type: "function",
Function: types.FunctionDef{
Name: w.Name(),
Description: w.Description(),
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"number": map[string]interface{}{
"type": "string",
"description": "订单编号/流水号",
},
},
"required": []string{"number"},
},
},
}
}
// ZltxOrderDetailRequest 直连天下订单详情请求参数
type ZltxOrderDetailRequest struct {
Number string `json:"number"`
}
// ZltxOrderDetailResponse 直连天下订单详情响应
type ZltxOrderDetailResponse struct {
Code int `json:"code"`
Error string `json:"error"`
Data ZltxOrderDetailData `json:"data"`
}
// ZltxOrderDetailData 直连天下订单详情数据
type ZltxOrderDetailData struct {
Direct map[string]any `json:"direct"`
Order map[string]any `json:"order"`
}
// Execute 执行直连天下订单详情查询
func (w *ZltxOrderDetailTool) Execute(ctx context.Context, args json.RawMessage) (interface{}, error) {
var req ZltxOrderDetailRequest
if err := json.Unmarshal(args, &req); err != nil {
return nil, fmt.Errorf("invalid zltxOrderDetail request: %w", err)
}
if req.Number == "" {
return nil, fmt.Errorf("number is required")
}
// 这里可以集成真实的直连天下订单详情API
return w.getZltxOrderDetail(ctx, req.Number), nil
}
// getMockZltxOrderDetail 获取模拟直连天下订单详情数据
func (w *ZltxOrderDetailTool) getZltxOrderDetail(ctx context.Context, number string) *ZltxOrderDetailResponse {
url := fmt.Sprintf("%s/admin/direct/ai/%s", w.config.BaseURL, number)
authorization := fmt.Sprintf("Bearer %s", w.config.APIKey)
// 发送http请求
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return &ZltxOrderDetailResponse{}
}
req.Header.Set("Authorization", authorization)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return &ZltxOrderDetailResponse{}
}
defer resp.Body.Close()
return &ZltxOrderDetailResponse{}
}

View File

@ -7,15 +7,23 @@ import (
// ChatRequest 聊天请求
type ChatRequest struct {
Message string `json:"message" binding:"required"`
Model string `json:"model,omitempty"`
UserInput string `json:"user_input" binding:"required"`
Caller string `json:"caller" binding:"required"`
SessionID string `json:"session_id"`
ChatRequestMeta ChatRequestMeta `json:"meta,omitempty"`
}
// ChatRequestMeta 聊天请求元数据
type ChatRequestMeta struct {
Authorization string `json:"authorization"`
}
// ChatResponse 聊天响应
type ChatResponse struct {
Message string `json:"message"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Finished bool `json:"finished"`
Status string `json:"status"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
TaskCode string `json:"task_code,omitempty"`
}
// ToolCall 工具调用
@ -34,8 +42,8 @@ type FunctionCall struct {
// ToolDefinition 工具定义
type ToolDefinition struct {
Type string `json:"type"`
Function FunctionDef `json:"function"`
Type string `json:"type"`
Function FunctionDef `json:"function"`
}
// FunctionDef 函数定义
@ -67,4 +75,4 @@ type Message struct {
// RouterService 路由服务接口
type RouterService interface {
Route(ctx context.Context, req *ChatRequest) (*ChatResponse, error)
}
}