💻 源码
import { chatCompletion, MAX_LOOPS } from './dashscope'
import { groupStepsIntoRounds, countToolCalls } from './rounds'
import { resolveThought } from './thought'
import { executeTool, TOOL_DEFINITIONS, TOOL_LABELS } from './tools'
import type { ChatMessage, ReActStep, RunResult } from '../types'

const SYSTEM_PROMPT = `你是 ReAct(Reasoning + Acting)模式的教学演示助手。

每轮严格遵循:
1. Thought:先分析用户问题,说明是否需要工具、为什么
2. Action:需要实时数据时,调用对应工具(每次一个)
3. Observation:等待工具返回,不要编造
4. 拿到 Observation 后,再 Thought 一次,然后给出最终 Answer

硬性规则:
- 天气、当前时间、大数计算、汇率 → 必须调用工具
- 最终回答必须引用工具返回的真实数字
- 不要跳过思考步骤,即使你会同时发起工具调用`

function uid(): string {
  return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}

function toAssistantMessage(msg: Awaited<ReturnType<typeof chatCompletion>>): ChatMessage {
  return {
    role: 'assistant',
    content: msg.content ?? '',
    tool_calls: msg.tool_calls,
  }
}

export async function runReAct(
  apiKey: string,
  userQuestion: string,
  onStep?: (step: ReActStep) => void,
): Promise<RunResult> {
  const steps: ReActStep[] = []
  const messages: ChatMessage[] = [
    { role: 'system', content: SYSTEM_PROMPT },
    { role: 'user', content: userQuestion },
  ]

  const push = (step: ReActStep) => {
    steps.push(step)
    onStep?.(step)
  }

  push({
    id: uid(),
    type: 'input',
    round: 0,
    title: '用户提问',
    content: userQuestion,
  })

  let loops = 0
  let finalAnswer = ''
  let hadToolCalls = false

  while (loops < MAX_LOOPS) {
    loops++
    const reply = await chatCompletion(apiKey, messages, TOOL_DEFINITIONS)
    messages.push(toAssistantMessage(reply))

    if (!reply.tool_calls?.length) {
      const thought = resolveThought({
        userQuestion,
        round: loops,
        modelContent: reply.content,
        modelReasoning: reply.reasoning_content,
        phase: hadToolCalls ? 'synthesis_answer' : 'direct_answer',
        hadToolCalls,
      })

      push({
        id: uid(),
        type: 'thought',
        round: loops,
        title: `第 ${loops} 轮 · 思考`,
        content: thought,
      })

      finalAnswer = reply.content?.trim() || '(模型未返回内容)'
      push({
        id: uid(),
        type: 'answer',
        round: loops,
        title: '最终回答',
        content: finalAnswer,
      })
      break
    }

    const call = reply.tool_calls[0]
    const toolName = call.function.name
    const toolArgs = call.function.arguments
    const label = TOOL_LABELS[toolName] ?? toolName
    hadToolCalls = true

    push({
      id: uid(),
      type: 'thought',
      round: loops,
      title: `第 ${loops} 轮 · 思考`,
      content: resolveThought({
        userQuestion,
        round: loops,
        modelContent: reply.content,
        modelReasoning: reply.reasoning_content,
        phase: 'before_tool',
        toolName,
        toolArgs,
      }),
    })

    push({
      id: uid(),
      type: 'action',
      round: loops,
      title: `第 ${loops} 轮 · 行动`,
      content: `调用工具:${label}(${toolName})`,
      meta: { tool: toolName, arguments: toolArgs },
    })

    const result = await executeTool(toolName, toolArgs)

    push({
      id: uid(),
      type: 'observation',
      round: loops,
      title: `第 ${loops} 轮 · 观察`,
      content: result,
      meta: { tool: toolName },
    })

    messages.push({
      role: 'tool',
      tool_call_id: call.id,
      name: toolName,
      content: result,
    })
  }

  if (!finalAnswer && loops >= MAX_LOOPS) {
    finalAnswer = '已达到最大推理轮次,请简化问题后重试。'
    push({
      id: uid(),
      type: 'answer',
      round: loops,
      title: '最终回答',
      content: finalAnswer,
    })
  }

  return {
    steps,
    rounds: groupStepsIntoRounds(steps),
    finalAnswer,
    toolCallCount: countToolCalls(steps),
    messages,
  }
}