projects/repobot/v3/agent.py

49 行 · 2.7 KB

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

"""RepoBot v3 的智能体循环,和 05 模块第 2 课的写法相同,加上了费用统计和执行轨迹。"""
import json

import llm
from tools import TOOLS

SYSTEM = """你是 RepoBot,Python HTTP 客户端库 httpx 的答疑助手。你可以查 httpx 的官方文档和源码。

做法:
- 先用 search_docs 查文档。文档里有答案,就根据文档回答。
- 文档里没有答案(比如默认值、内部逻辑、某个异常什么时候抛出),再用 grep_source 和 read_source 查源码。
- 回答里注明依据:文档写成 [文档 文件名],源码写成 [源码 文件路径:行号]。
- 只根据查到的内容回答。查了还是找不到,就如实说没有找到,不要猜。
- 和 httpx 无关的问题,直接礼貌地说明你只负责 httpx,不要调用任何工具。
- 用中文回答,简洁,代码保持原样。"""

MAX_STEPS = 10
MAX_OBSERVATION = 4000


def run(question, history=(), show=print):
    """回答一个问题。返回 (回答, 统计)。history 是之前几轮的 user/assistant 消息。"""
    messages = [{"role": "system", "content": SYSTEM}, *history, {"role": "user", "content": question}]
    schemas = [t["schema"] for t in TOOLS.values()]
    stats = {"steps": 0, "tool_calls": 0, "cost": 0.0, "prompt_tokens": 0}
    for step in range(1, MAX_STEPS + 1):
        response = llm.with_retry(lambda: llm.client.chat.completions.create(
            model=llm.MODEL, messages=messages, tools=schemas, extra_body={"thinking": {"type": "disabled"}}))
        message = response.choices[0].message
        stats["steps"] = step
        stats["cost"] += llm.cost_usd(response.usage)
        stats["prompt_tokens"] += response.usage.prompt_tokens
        if not message.tool_calls:
            return message.content, stats
        messages.append(message.model_dump(exclude_none=True))
        for call in message.tool_calls:
            stats["tool_calls"] += 1
            try:
                result = TOOLS[call.function.name]["fn"](**json.loads(call.function.arguments or "{}"))
            except KeyError:
                result = f"错误:没有叫 {call.function.name} 的工具"
            except Exception as e:
                result = f"错误:{type(e).__name__}: {e}"
            if len(result) > MAX_OBSERVATION:
                result = result[:MAX_OBSERVATION] + f"\n……(已截断,共 {len(result)} 字符)"
            show(f"  [{step}] {call.function.name}({call.function.arguments}) → {result.splitlines()[0][:70] if result else ''}")
            messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
    return "这个问题查了很多步还没有得出结论,我先停下来。可以换个问法,或者问得更具体一些。", stats