intelligent_test_assessment.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. package service
  2. import (
  3. "encoding/json"
  4. "io"
  5. "math/big"
  6. "reflect"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "golang.org/x/text/unicode/norm"
  11. )
  12. const IntelligentEvaluatorVersion = 3
  13. var (
  14. intelligentFinalAnswer = regexp.MustCompile(`(?i)^(?:ANSWER|FINAL ANSWER|最终答案|最终结果|答案|答)\s*(?:[::]|是|为)\s*(.+)$`)
  15. intelligentConclusion = regexp.MustCompile(`^(?:所以|因此|最后|最终|综上|故).*(?:剩下|还剩|剩余|有|是|为)\s*([+-]?[0-9]+(?:\.[0-9]+)?\s*[^0-9]*)$`)
  16. intelligentNumber = regexp.MustCompile(`^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]{1,3})?(?:/[+-]?[0-9]+)?$`)
  17. intelligentGroupedNumber = regexp.MustCompile(`^[+-]?[0-9]{1,3}(?:,[0-9]{3})+(?:\.[0-9]+)?$`)
  18. intelligentAmbiguousConclusion = regexp.MustCompile(`不|没|无|非|假设|假如|如果|若|可能|也许|例如|举例|是否|至少|至多|大概|约|或者|[??“”「」]`)
  19. )
  20. func newIntelligentAssessment(method string) map[string]any {
  21. return map[string]any{
  22. "evaluator_version": IntelligentEvaluatorVersion, "method": method,
  23. "execution_status": "completed", "answer_verdict": "undetermined",
  24. "format_verdict": "non_compliant", "capability_verdict": "insufficient_evidence",
  25. "limitation": "仅评价本次最终答案与输出格式,未验证完整推导;单次测试不足以判断模型能力下降。",
  26. }
  27. }
  28. func intelligentAnswerUnit(cfg IntelligentTestConfig) string {
  29. if cfg.AnswerUnitMode == "none" {
  30. return ""
  31. }
  32. if unit := strings.TrimSpace(cfg.AnswerUnit); unit != "" {
  33. return unit
  34. }
  35. if cfg.AnswerUnitMode == "configured" {
  36. return ""
  37. }
  38. // Compatibility for the original candy question's saved snapshots only.
  39. if strings.Contains(strings.ReplaceAll(cfg.Prompt, " ", ""), "盒子里有24颗糖。小明取走总数的四分之一") {
  40. return "颗"
  41. }
  42. return ""
  43. }
  44. func cleanIntelligentAnswerLine(line string) string {
  45. line = strings.TrimSpace(norm.NFKC.String(line))
  46. line = strings.TrimSpace(strings.TrimPrefix(line, ">"))
  47. for _, pair := range [][2]string{{"**", "**"}, {"__", "__"}, {"`", "`"}} {
  48. if len(line) >= len(pair[0])+len(pair[1]) && strings.HasPrefix(line, pair[0]) && strings.HasSuffix(line, pair[1]) {
  49. line = strings.TrimSpace(line[len(pair[0]) : len(line)-len(pair[1])])
  50. }
  51. }
  52. return line
  53. }
  54. func normalizeIntelligentNumber(value, unit string) (string, bool) {
  55. value = cleanIntelligentAnswerLine(value)
  56. value = strings.TrimSpace(strings.TrimRight(value, "。.!!"))
  57. if len(value) == 0 || len(value) > 256 {
  58. return "", false
  59. }
  60. if unit != "" {
  61. units := []string{unit}
  62. if unit == "颗" {
  63. units = []string{"颗糖", "颗"}
  64. }
  65. for _, suffix := range units {
  66. if strings.HasSuffix(value, suffix) {
  67. value = strings.TrimSpace(strings.TrimSuffix(value, suffix))
  68. break
  69. }
  70. }
  71. }
  72. if intelligentGroupedNumber.MatchString(value) {
  73. value = strings.ReplaceAll(value, ",", "")
  74. }
  75. if !intelligentNumber.MatchString(value) {
  76. return "", false
  77. }
  78. if exponent := strings.IndexAny(value, "eE"); exponent >= 0 {
  79. e, err := strconv.Atoi(value[exponent+1:])
  80. if err != nil || e < -100 || e > 100 {
  81. return "", false
  82. }
  83. }
  84. number, ok := new(big.Rat).SetString(value)
  85. if !ok {
  86. return "", false
  87. }
  88. return number.RatString(), true
  89. }
  90. func intelligentAnswerCandidates(output string, cfg IntelligentTestConfig) []string {
  91. clean := strings.TrimSpace(output)
  92. if strings.HasPrefix(clean, "```") {
  93. if first := strings.IndexByte(clean, '\n'); first >= 0 && strings.HasSuffix(clean, "```") {
  94. clean = strings.TrimSpace(clean[first+1 : len(clean)-3])
  95. }
  96. }
  97. if candidates, ok := intelligentJSONAnswerCandidates(clean); ok && len(candidates) > 0 {
  98. return candidates
  99. }
  100. var explicit, conclusions []string
  101. last := ""
  102. lastMarked := false
  103. for _, line := range strings.Split(clean, "\n") {
  104. if strings.HasPrefix(strings.TrimSpace(line), ">") {
  105. continue
  106. }
  107. line = cleanIntelligentAnswerLine(line)
  108. if line == "" || strings.HasPrefix(line, "```") {
  109. continue
  110. }
  111. last = line
  112. lastMarked = false
  113. if match := intelligentFinalAnswer.FindStringSubmatch(line); match != nil {
  114. lastMarked = true
  115. explicit = append(explicit, cleanIntelligentAnswerLine(match[1]))
  116. continue
  117. }
  118. if match := intelligentConclusion.FindStringSubmatch(line); match != nil {
  119. // A numeric mention in a negated/hypothetical conclusion is not an
  120. // affirmative final answer, even when it matches the expected number.
  121. if intelligentAmbiguousConclusion.MatchString(line) {
  122. return nil
  123. }
  124. lastMarked = true
  125. conclusions = append(conclusions, match[1])
  126. }
  127. }
  128. if !lastMarked && cfg.AnswerType != "text" && len(explicit)+len(conclusions) > 0 {
  129. if _, ok := normalizeIntelligentNumber(last, intelligentAnswerUnit(cfg)); ok {
  130. conclusions = append(conclusions, last)
  131. }
  132. }
  133. if len(explicit) > 0 {
  134. return append(explicit, conclusions...)
  135. }
  136. if len(conclusions) > 0 {
  137. return conclusions
  138. }
  139. if last != "" {
  140. return []string{last}
  141. }
  142. return nil
  143. }
  144. func intelligentJSONAnswerCandidates(output string) ([]string, bool) {
  145. d := json.NewDecoder(strings.NewReader(output))
  146. token, err := d.Token()
  147. if err != nil || token != json.Delim('{') {
  148. return nil, false
  149. }
  150. var candidates []string
  151. for d.More() {
  152. key, err := d.Token()
  153. if err != nil {
  154. return nil, false
  155. }
  156. var raw json.RawMessage
  157. if err := d.Decode(&raw); err != nil {
  158. return nil, false
  159. }
  160. if key == "answer" || key == "final_answer" || key == "答案" {
  161. var value string
  162. if json.Unmarshal(raw, &value) != nil {
  163. value = string(raw)
  164. }
  165. candidates = append(candidates, value)
  166. }
  167. }
  168. if _, err := d.Token(); err != nil {
  169. return nil, false
  170. }
  171. var trailing any
  172. if d.Decode(&trailing) != io.EOF {
  173. return nil, false
  174. }
  175. return candidates, true
  176. }
  177. func evaluateIntelligentAnswer(output string, cfg IntelligentTestConfig) IntelligentTestEvaluation {
  178. detail := newIntelligentAssessment("exact_answer")
  179. detail["expected_answer"] = cfg.ExpectedAnswer
  180. last := strings.TrimSpace(output)
  181. if pos := strings.LastIndexByte(last, '\n'); pos >= 0 {
  182. last = strings.TrimSpace(last[pos+1:])
  183. }
  184. if cfg.AnswerFormat == "free_text" {
  185. detail["format_verdict"], detail["format_reason"] = "not_required", "本题未要求固定答案行格式"
  186. } else if len(answerLinePattern.FindAllStringSubmatch(output, -1)) == 1 && answerLinePattern.MatchString(last) {
  187. detail["format_verdict"], detail["format_reason"] = "compliant", "符合唯一末行 ANSWER: 答案格式"
  188. } else {
  189. detail["format_reason"] = "未符合唯一末行 ANSWER: 答案格式,答案正确性单独判断"
  190. }
  191. unit := intelligentAnswerUnit(cfg)
  192. expected, numeric := normalizeIntelligentNumber(cfg.ExpectedAnswer, unit)
  193. if cfg.AnswerType == "text" {
  194. numeric = false
  195. }
  196. if cfg.AnswerType == "number" && !numeric {
  197. detail["reason"] = "标准答案不是有效数值,请检查测试设置"
  198. return IntelligentTestEvaluation{Status: "completed", Detail: detail}
  199. }
  200. if !numeric {
  201. expected = cleanIntelligentAnswerLine(cfg.ExpectedAnswer)
  202. }
  203. detail["answer_type"] = "text"
  204. if numeric {
  205. detail["answer_type"] = "number"
  206. }
  207. candidates := intelligentAnswerCandidates(output, cfg)
  208. detail["candidate_count"] = len(candidates)
  209. preview := candidates
  210. if len(preview) > 10 {
  211. preview = preview[:10]
  212. }
  213. detail["candidate_answers"] = preview
  214. values := map[string]bool{}
  215. for _, candidate := range candidates {
  216. value := cleanIntelligentAnswerLine(candidate)
  217. if numeric {
  218. var ok bool
  219. value, ok = normalizeIntelligentNumber(candidate, unit)
  220. if !ok {
  221. detail["reason"] = "最终答案包含无法确认的数值或单位,需要复核"
  222. return IntelligentTestEvaluation{Status: "completed", Detail: detail}
  223. }
  224. }
  225. values[value] = true
  226. }
  227. if len(values) != 1 || expected == "" {
  228. detail["reason"] = "没有唯一、无歧义的最终答案,需要复核"
  229. return IntelligentTestEvaluation{Status: "completed", Detail: detail}
  230. }
  231. var actual string
  232. for value := range values {
  233. actual = value
  234. }
  235. detail["actual_answer"] = candidates[0]
  236. detail["normalized_answer"], detail["normalized_expected"] = actual, expected
  237. score := 0.0
  238. if actual == expected {
  239. score = 100
  240. detail["answer_verdict"], detail["reason"] = "correct", "最终答案与标准答案一致;完整推导未单独验证"
  241. } else {
  242. detail["answer_verdict"], detail["reason"] = "incorrect", "最终答案与标准答案不一致;单次结果不作为能力下降结论"
  243. }
  244. return IntelligentTestEvaluation{Status: "completed", Score: &score, Detail: detail}
  245. }
  246. func SameIntelligentTestConfig(a, b IntelligentTestConfig) bool {
  247. canonical := func(c IntelligentTestConfig) IntelligentTestConfig {
  248. c.Execution = nil
  249. c.Prompt = strings.TrimSpace(c.Prompt)
  250. c.Model = strings.TrimSpace(c.Model)
  251. c.ExpectedAnswer = strings.TrimSpace(c.ExpectedAnswer)
  252. if c.AnswerType == "" {
  253. c.AnswerType = "auto"
  254. }
  255. if c.AnswerFormat == "" {
  256. c.AnswerFormat = "answer_line"
  257. }
  258. if c.AnswerUnitMode == "" {
  259. c.AnswerUnitMode = "legacy"
  260. if c.AnswerUnit != "" {
  261. c.AnswerUnitMode = "configured"
  262. }
  263. }
  264. c.AnswerUnit = intelligentAnswerUnit(c)
  265. return c
  266. }
  267. return reflect.DeepEqual(canonical(a), canonical(b))
  268. }
  269. func IsCurrentIntelligentAssessment(evaluation map[string]any) bool {
  270. return mode1Int(evaluation["evaluator_version"]) == IntelligentEvaluatorVersion
  271. }
  272. func PublicIntelligentAssessment(evaluation map[string]any) map[string]any {
  273. version := mode1Int(evaluation["evaluator_version"])
  274. if version < 2 || version > IntelligentEvaluatorVersion {
  275. return nil
  276. }
  277. out := map[string]any{"evaluator_version": version, "capability_verdict": "insufficient_evidence"}
  278. for _, key := range []string{"answer_verdict", "format_verdict", "image_state", "execution_status"} {
  279. if value, ok := evaluation[key].(string); ok {
  280. out[key] = value
  281. }
  282. }
  283. return out
  284. }