package api import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) // CompanyIndustryClassifier 企业行业分类器 type CompanyIndustryClassifier struct { httpClient *http.Client llmClient *LLMClient } // CompanyData 企业数据 type CompanyData struct { Description string `json:"description"` Business string `json:"business"` BiddingTitles []string `json:"bidding_titles"` } // IndustryClassification 行业分类结果 type IndustryClassification struct { CompanyName string `json:"company_name"` Primary PrimaryInfo `json:"primary"` Secondary []Secondary `json:"secondary"` } // PrimaryInfo 主要行业信息 type PrimaryInfo struct { Code string `json:"code"` Name string `json:"name"` Reason string `json:"reason"` } // Secondary 次要行业信息 type Secondary struct { Code string `json:"code"` Name string `json:"name"` Reason string `json:"reason"` } // NewCompanyIndustryClassifier 创建企业行业分类器 func NewCompanyIndustryClassifier(llmClient *LLMClient) *CompanyIndustryClassifier { client := &http.Client{ Timeout: 30 * time.Second, } return &CompanyIndustryClassifier{ httpClient: client, llmClient: llmClient, } } // getDescriptionText 获取简介文本 func getDescriptionText(description string) string { if description == "" { return "(无简介)" } return description } // getBusinessText 获取经营范围文本 func getBusinessText(business string) string { if business == "" { return "(无经营范围)" } return business } // FetchCompanyData 从cmdp API获取企业工商和招投标数据 func (c *CompanyIndustryClassifier) FetchCompanyData(companyName string) (map[string]interface{}, error) { apiURL := "https://a.cmdp.cn/cmds/v1/company/info-rest/custom" params := url.Values{} params.Set("name", companyName) params.Set("api", "p_info,p_biddings_all") fullURL := apiURL + "?" + params.Encode() req, err := http.NewRequest("GET", fullURL, nil) if err != nil { return nil, fmt.Errorf("创建请求失败: %v", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("发送请求失败: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body)) } var result map[string]interface{} err = json.Unmarshal(body, &result) if err != nil { return nil, fmt.Errorf("解析响应JSON失败: %v", err) } return result, nil } // ExtractFields 从API响应中提取关键字段 func (c *CompanyIndustryClassifier) ExtractFields(data map[string]interface{}) (string, string, []string) { description := "" business := "" var biddingTitles []string // 兼容不同的响应结构 payload := data if data["data"] != nil { if payloadData, ok := data["data"].(map[string]interface{}); ok { payload = payloadData } } // p_info -> description, business if pInfo := payload["p_info"]; pInfo != nil { var pInfoObj map[string]interface{} switch v := pInfo.(type) { case []interface{}: if len(v) > 0 { if item, ok := v[0].(map[string]interface{}); ok { pInfoObj = item } } case map[string]interface{}: pInfoObj = v } if pInfoObj != nil { if desc, ok := pInfoObj["description"].(string); ok { description = desc } if bus, ok := pInfoObj["business"].(string); ok { business = bus } } } // p_biddings_all -> title list if biddingsRaw := payload["p_biddings_all"]; biddingsRaw != nil { var biddingsList []interface{} switch v := biddingsRaw.(type) { case []interface{}: biddingsList = v case map[string]interface{}: // 尝试从data或list字段获取 if dataField, ok := v["data"].([]interface{}); ok { biddingsList = dataField } else if listField, ok := v["list"].([]interface{}); ok { biddingsList = listField } } for _, item := range biddingsList { if itemMap, ok := item.(map[string]interface{}); ok { if title, ok := itemMap["title"].(string); ok && strings.TrimSpace(title) != "" { biddingTitles = append(biddingTitles, strings.TrimSpace(title)) } } } } return description, business, biddingTitles } // BuildPrompt 构建提示词 func (c *CompanyIndustryClassifier) BuildPrompt(companyName, description, business string, biddingTitles []string) string { titlesText := "(无招投标记录)" if len(biddingTitles) > 0 { var sb strings.Builder for i, title := range biddingTitles { sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, title)) } titlesText = sb.String() } prompt := fmt.Sprintf(`你是国民经济行业分类专家,请根据以下企业信息,依据《国民经济行业分类》(GB/T 4754-2017)进行行业大类判断。 ## 企业名称 %s ## 企业简介(description) %s ## 经营范围(business) %s ## 招投标项目标题(bidding titles) %s ## 任务要求 1. 判断该企业**主要**所属的行业大类(两位数字编号 + 名称) 2. 如有次要业务涉及其他大类,也一并列出(最多2个) 3. 给出简要判断依据(不超过100字) ## 输出格式(严格 JSON,不要 Markdown 代码块) { "company_name": "%s", "primary": { "code": "65", "name": "软件和信息技术服务业", "reason": "判断依据..." }, "secondary": [ { "code": "72", "name": "商务服务业", "reason": "轻微涉及原因..." } ] } 注意:secondary 可以为空列表 [];只输出 JSON,不要任何额外说明。`, companyName, getDescriptionText(description), getBusinessText(business), titlesText, companyName) return prompt } // ClassifyCompany 完整流程:获取数据 → 提取字段 → 构建提示词 → 调用 LLM → 返回 JSON func (c *CompanyIndustryClassifier) ClassifyCompany(companyName string) (*IndustryClassification, error) { // 1. 获取企业数据 rawData, err := c.FetchCompanyData(companyName) if err != nil { return nil, fmt.Errorf("获取企业数据失败: %v", err) } // 2. 提取关键字段 description, business, biddingTitles := c.ExtractFields(rawData) // 3. 构建提示词 prompt := c.BuildPrompt(companyName, description, business, biddingTitles) // 4. 调用LLM并解析JSON response, err := c.llmClient.Chat([]ChatMessage{ {Role: "user", Content: prompt}, }) if err != nil { return nil, fmt.Errorf("调用LLM失败: %v", err) } // 提取JSON部分(如果LLM返回了markdown代码块) jsonStr := extractJSONFromResponse(response.Choices[0].Message.Content) var result IndustryClassification err = json.Unmarshal([]byte(jsonStr), &result) if err != nil { return nil, fmt.Errorf("解析LLM返回的JSON失败: %v", err) } return &result, nil } // extractJSONFromResponse 从LLM响应中提取JSON内容 // 查找并提取 ```json {...} ``` 或 ``` {...} ``` 中的内容 func extractJSONFromResponse(response string) string { // 首先尝试匹配 ```json 开头的代码块 start := strings.Index(response, "```json") if start != -1 { start += 7 // 跳过 "```json" end := strings.Index(response[start:], "```") if end != -1 { jsonContent := strings.TrimSpace(response[start : start+end]) return jsonContent } } // 然后尝试匹配 ``` 开头的代码块(假设是JSON) start = strings.Index(response, "```") if start != -1 { start += 3 // 跳过 "```" end := strings.Index(response[start:], "```") if end != -1 { jsonContent := strings.TrimSpace(response[start : start+end]) // 检查是否看起来像JSON jsonContent = strings.TrimSpace(jsonContent) if strings.HasPrefix(jsonContent, "{") || strings.HasPrefix(jsonContent, "[") { return jsonContent } } } // 如果没有找到代码块,尝试直接解析整个响应是否为JSON response = strings.TrimSpace(response) if strings.HasPrefix(response, "{") && strings.HasSuffix(response, "}") || strings.HasPrefix(response, "[") && strings.HasSuffix(response, "]") { return response } // 如果以上都失败,返回原内容 return response }