feat: 行程规划改用 tool-calling agent,旧逻辑保留为降级方案
ai.ts 新增通用 runAgentLoop(),blindboxPlanGen.ts 拆分为 agent 主路径(list_ideas/search_poi/finalize_plan 三个工具) 和 legacy 降级路径,agent 失败时自动回退。 AI 思考和工具调用实时推送给前端。
This commit is contained in:
+134
@@ -1,4 +1,8 @@
|
||||
import OpenAI from "openai";
|
||||
import type {
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionTool,
|
||||
} from "openai/resources/chat/completions";
|
||||
import type { IdeaTags, PlanItem } from "@/types";
|
||||
|
||||
function getClient() {
|
||||
@@ -224,3 +228,133 @@ ${Object.entries(ctx.candidates)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generic tool-calling agent loop
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface AgentTool {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>; // JSON Schema for the tool parameters
|
||||
execute: (args: Record<string, unknown>) => Promise<string>;
|
||||
progressBefore?: (args: Record<string, unknown>) => string;
|
||||
progressAfter?: (args: Record<string, unknown>, result: string) => string;
|
||||
}
|
||||
|
||||
export interface AgentLoopOptions {
|
||||
systemPrompt: string;
|
||||
userPrompt: string;
|
||||
tools: AgentTool[];
|
||||
onProgress?: (message: string) => void;
|
||||
maxTurns?: number;
|
||||
}
|
||||
|
||||
export interface AgentLoopResult {
|
||||
finalArgs: Record<string, unknown>;
|
||||
turns: number;
|
||||
}
|
||||
|
||||
export async function runAgentLoop(
|
||||
options: AgentLoopOptions,
|
||||
): Promise<AgentLoopResult | null> {
|
||||
const { systemPrompt, userPrompt, tools, onProgress, maxTurns = 15 } = options;
|
||||
const client = getClient();
|
||||
|
||||
const openaiTools: ChatCompletionTool[] = tools.map((t) => ({
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: t.parameters,
|
||||
},
|
||||
}));
|
||||
|
||||
const toolMap = new Map(tools.map((t) => [t.name, t]));
|
||||
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
];
|
||||
|
||||
for (let turn = 0; turn < maxTurns; turn++) {
|
||||
const response = await client.chat.completions.create({
|
||||
model: "deepseek-chat",
|
||||
messages,
|
||||
tools: openaiTools,
|
||||
temperature: 0.6,
|
||||
max_tokens: 4096,
|
||||
});
|
||||
|
||||
const choice = response.choices[0];
|
||||
if (!choice) break;
|
||||
|
||||
const assistantMessage = choice.message;
|
||||
|
||||
// Push AI thinking text to the user
|
||||
if (assistantMessage.content) {
|
||||
onProgress?.(assistantMessage.content);
|
||||
}
|
||||
|
||||
// No tool calls — conversation ended
|
||||
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
||||
// Add the assistant message and break if there's nothing more to do
|
||||
messages.push({ role: "assistant", content: assistantMessage.content ?? "" });
|
||||
if (choice.finish_reason === "stop") break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the assistant message (with tool_calls) to history
|
||||
messages.push(assistantMessage as ChatCompletionMessageParam);
|
||||
|
||||
// Execute each tool call
|
||||
for (const toolCall of assistantMessage.tool_calls) {
|
||||
if (toolCall.type !== "function") continue;
|
||||
const fnName = toolCall.function.name;
|
||||
const tool = toolMap.get(fnName);
|
||||
if (!tool) {
|
||||
messages.push({
|
||||
role: "tool",
|
||||
tool_call_id: toolCall.id,
|
||||
content: JSON.stringify({ error: `Unknown tool: ${fnName}` }),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let args: Record<string, unknown>;
|
||||
try {
|
||||
args = JSON.parse(toolCall.function.arguments);
|
||||
} catch {
|
||||
args = {};
|
||||
}
|
||||
|
||||
// Progress: before execution
|
||||
if (tool.progressBefore) {
|
||||
onProgress?.(tool.progressBefore(args));
|
||||
}
|
||||
|
||||
// Execute
|
||||
const result = await tool.execute(args);
|
||||
|
||||
// Progress: after execution
|
||||
if (tool.progressAfter) {
|
||||
onProgress?.(tool.progressAfter(args, result));
|
||||
}
|
||||
|
||||
// If this is finalize_plan, return immediately
|
||||
if (fnName === "finalize_plan") {
|
||||
return { finalArgs: args, turns: turn + 1 };
|
||||
}
|
||||
|
||||
// Feed tool result back to the conversation
|
||||
messages.push({
|
||||
role: "tool",
|
||||
tool_call_id: toolCall.id,
|
||||
content: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Exhausted turns without finalize
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user