En este tutorial, creamos un agente de razonamiento avanzado de múltiples ramas de Árbol de pensamientos (ToT) desde cero. En lugar de depender de un razonamiento lineal en cadena de pensamiento, diseñamos un sistema que genera múltiples ramas de razonamiento, califica cada rama usando una función de evaluación heurística, elimina los candidatos débiles y continúa expandiendo solo los caminos más fuertes. Combinamos un modelo de transformador ajustado por instrucciones con una estructura de árbol personalizada e implementamos la selección de estilo de búsqueda de haz con búsqueda de profundidad limitada. Al basar el sistema en el dominio de 24 juegos, creamos un punto de referencia claro y objetivo para el razonamiento donde podemos observar la expansión de ramas, la poda, la puntuación y la detección de goles en acción.
exprs: lista[str]
pensamiento: str = “” puntuación: float = -1e9 is_goal: bool = False padre: Opcional[“Node”] = Ninguno meta: Dict[str, Any] = campo(default_factory=dict) def Pretty_state(numeros: Lista[float]exprs: Lista[str]) -> cadena: pares = [f”{e}={n:g}” for e, n in zip(exprs, nums)]
devolver ” | “.join(pares)
Instalamos las bibliotecas necesarias y cargamos el modelo FLAN-T5 utilizando la arquitectura Seq2Seq correcta. Definimos nuestra estructura de datos de Nodo central que representa cada estado de razonamiento en la búsqueda del Árbol de Pensamientos. También inicializamos la configuración del dispositivo y las utilidades auxiliares que nos permiten imprimir e inspeccionar claramente el estado de razonamiento.
def safe_apply(a: flotante, b: flotante, op: str) -> Opcional[float]: si op == “+”: devuelve a + b si op == “-“: devuelve a – b si op == “*”: devuelve a * b si op == “/”: si abs(b) < 1e-12: devuelve Ninguno devuelve a / b devuelve Ninguno def combine_expr(ea: str, eb: str, op: str) -> str: return f”({ea} {op} {eb})” def is_24(x: float, tol: float = 1e-6) -> bool: return abs(x – 24.0) <= tol def one_step_closeness(nums: Lista[float]) -> flotante: si len(nums) == 1: devuelve abs(nums)[0] – 24.0) best = float(“inf”) n = len(nums) para i en rango(n): para j en rango(n): si i == j: continuar a, b = nums[i]números[j]
para op en OPS: r = safe_apply(a, b, op) si r es Ninguno: continuar mejor = min(mejor, abs(r – 24.0)) devolver mejor si es mejor!= float(“inf”) else 1e9 def heuristic_score(node: Node) -> float: nums = node.numbers base = -one_step_closeness(nums) Depth_penalty = 0.05 * nodo.profundidad exactitud_bonus = 2,0 si existe (is_24(x) para x en números) de lo contrario 0,0 base de retorno – profundidad_penalización + bonificación_exacta
Implementamos la lógica matemática del dominio de 24 juegos. Definimos la ejecución segura del operador, la construcción de expresiones, la verificación de objetivos y una función de puntuación heurística que estima qué tan cerca está un estado del objetivo de 24. Diseñamos la heurística para guiar la búsqueda de manera inteligente mientras penalizamos las ramas más profundas.
para línea en text.splitlines(): línea = line.strip() m = re.match(r”^\s*(\d+)\s*,\s*(\d+)\s*,\s*([\+\-\*\/])\s*$”, línea) si no m: continuar i, j, op = int(m.group(1)), int(m.group(2)), m.group(3) si 0 <= i < n_items y 0 <= j < n_items and i != j: Moves.append((i, j, op)) visto = set() uniq = [] para mv en movimientos: si mv no está visto: uniq.append(mv) visto.add(mv) return uniq def fallback_moves(nums: Lista[float]límite: int = 24) -> Lista[Tuple[int, int, str]]: puntuado = []
n = len(nums) para i en rango(n): para j en rango(n): si i == j: continuar para op en OPS: r = safe_apply(nums[i]números[j]op) si r es Ninguno: continuar con scoring.append((abs(r – 24.0), i, j, op)) scoring.sort(key=lambda x: x[0]) fuera = [(i, j, op) for _, i, j, op in scored[:limit]]visto, uniq = set(), []
para mv in out: si mv no está visto: uniq.append(mv) visto.add(mv) devuelve uniq
Construimos el proponente de LLM que genera múltiples ramas de razonamiento. Formateamos el mensaje con cuidado para que el modelo devuelva operaciones de combinación estructuradas y analice esas salidas en movimientos ejecutables. También implementamos una estrategia alternativa determinista para garantizar que la búsqueda siga siendo sólida incluso si la salida del modelo es ruidosa.
exprs = nodo.exprs[:]
a, b = números[i]números[j]
r = safe_apply(a, b, op) si r es Ninguno: devuelve Ninguno ea, eb = exprs[i]exprés[j]
new_expr = combine_expr(ea, eb, op) para idx en ordenado([i, j]reverso=Verdadero): nums.pop(idx) exprs.pop(idx) nums.append(r) exprs.append(new_expr) child = Nodo( profundidad=nodo.profundidad + 1, números=nums, exprs=exprs, parent=nodo, pensamiento=f”Combinar elemento {i} y {j} con ‘{op}’ -> {new_expr} = {r:g}”, ) child.is_goal = (len(nums) == 1 y is_24(nums[0])) child.score = heuristic_score(child) return child def expandir(nodo: Nodo, factor_rama: int, proponer_kmin: int = 8, proponer_kmax: int = 14) -> Lista[Node]: items_str = “\n”.join([f”{idx}: {node.exprs[idx]} = {nodo.números[idx]:g}” para idx en rango(len(nodo.números))]) raw = llm_generate_suggestions(items_str, proponer_kmin, proponer_kmax) movimientos = parse_moves(raw, len(nodo.números)) si no se mueve: movimientos = fallback_moves(nodo.números, límite=30) movimientos = movimientos[: max(branch_factor * 2, branch_factor)]
niños = []
para (i, j, op) en movimientos: ch = apply_move(node, i, j, op) si ch no es Ninguno: niños.append(ch) niños.sort(key=lambda x: x.score, reverse=True) devuelve niños[:branch_factor]
Implementamos el mecanismo de expansión de ramas del algoritmo Árbol de pensamientos. Aplicamos los movimientos propuestos para crear nuevos nodos secundarios y calcular sus puntuaciones heurísticas. Luego podamos localmente las ramas más débiles, reteniendo sólo los candidatos más fuertes para una mayor exploración.
cur = objetivo mientras cur no es Ninguno: if cur.thinked: path.append(cur.thinked) cur = cur.parent return list(reversed(path)) def tot_solve_24( start_nums: Lista[int]ancho_viga: int = 10, factor_rama: int = 8, profundidad máxima: int = 3, umbral_prune: flotante = -10.0, detallado: bool = Verdadero) -> Dict[str, Any]: raíz = Nodo( profundidad=0, números=[float(x) for x in start_nums]exprs=[str(x) for x in start_nums]) root.score = heuristic_score(raíz) haz = [root]
best_seen = raíz si detallado: print(“\n=== ToT Search Start ===”) print(“Inicio:”, Pretty_state(root.numbers, root.exprs)) print(“Puntuación de raíz:”, root.score) para d en rango(max_profundidad): candidatos: Lista[Node] = []
si es detallado: print(f”\n— Profundidad {d} -> {d+1} expansión —“) print(“Estados del haz:”) para bidx, b en enumerar(haz[: min(len(beam), 6)]): imprimir(f” [{bidx}] puntuación={b.puntuación:.3f} | {pretty_state(b.numbers, b.exprs)}”) para b en la viga: niños = expandir(b, Branch_factor=branch_factor) candidatos.extend(kids) si no son candidatos: romper candidatos = [c for c in candidates if c.score >= prune_threshold]
goles = [c for c in candidates if c.is_goal]
si goles: goles.sort(key=lambda x: x.score, reverse=True) sol = goles[0]
pasos = reconstruct_solution(sol) return { “resuelto”: Verdadero, “inicio”: start_nums, “expresión”: sol.exprs[0]”valor”: sol.números[0]”pasos”: pasos, “final_score”: sol.score } candidatos.sort(key=lambda x: x.score, reverse=True) beam = candidatos[:beam_width]
si viga y viga[0].score > best_seen.score: best_seen = haz[0]
si es detallado: print(“Candidatos principales después de la poda/viga:”) para cidx, c en enumerar(viga[: min(len(beam), 6)]): imprimir(f” [{cidx}] puntuación={c.puntuación:.3f} | {pretty_state(c.números, c.exprs)}”) mejor_expr = mejor_visto.exprs[0] if len(best_seen.exprs) == 1 else “; “.join(best_seen.exprs) best_val = best_seen.numbers[0] if len(best_seen.numbers) == 1 else Ninguno return { “resuelto”: False, “start”: start_nums, “best_state”: Pretty_state(best_seen.numbers, best_seen.exprs), “best_expression”: best_expr, “best_value”: best_val, “final_score”: best_seen.score, “nota”: “No resuelto dentro de los límites de profundidad/haz; aumentar beam_width/branch_factor o ajustar la poda.” } pruebas = [
[4, 1, 8, 7],
[3, 3, 8, 8],
[6, 6, 6, 6],
[9, 9, 4, 4]]para números en pruebas: resultado = tot_solve_24( nums, beam_width=12, Branch_factor=10, max_profundidad=3, prune_threshold=-12.0, verbose=True ) print(“\n=== RESULTADO ===”) para k, v en result.items(): if k == “steps”: print(“steps:”) para s en v: print(” -“, s) else: print(f”{k}: {v}”) print(“\n” + “=”*80 + “\n”) print(“”” Para adaptar este agente ToT más allá del juego 24: 1) Defina una representación de ESTADO (como números/exprs aquí). 2) Defina un PROPONENTE que genere los siguientes pasos candidatos (herramienta LLM o basado en reglas). 3) Defina un HEURÍSTICO/ANOTADOR: – para comprobable tareas, use puntuación objetiva – para tareas abiertas, use una rúbrica de puntuación crítica de LLM 4) Ejecute el mismo ciclo ToT: expandir -> puntuación -> podar -> mantener la viga superior -> repetir hasta el objetivo o el límite de profundidad “””)
Implementamos el ciclo completo de búsqueda del Árbol de Pensamientos utilizando búsqueda de haz y límites de profundidad. Ampliamos, puntuamos, podamos y seleccionamos las ramas superiores en cada profundidad hasta que llegamos a una solución o agotamos el presupuesto de búsqueda. Finalmente, reconstruimos el camino del razonamiento y demostramos cómo el agente resuelve paso a paso múltiples instancias de 24 juegos.
En conclusión, construimos un agente de razonamiento completo de múltiples ramas que demuestra cómo Tree-of-Thoughts transforma el razonamiento LLM de un camino único a un proceso de búsqueda estructurado. Implementamos generación de ramas, puntuación heurística, poda, selección de haces y control de profundidad en una arquitectura modular que se puede adaptar fácilmente a otros problemas de razonamiento. A través de este tutorial, vimos cómo la combinación de modelos de lenguaje con algoritmos de búsqueda mejora significativamente la toma de decisiones estructurada. Ahora tenemos un marco ToT reutilizable que podemos extender al razonamiento matemático, tareas de planificación, búsqueda simbólica o incluso sistemas de evaluación basados en críticos LLM.
Consulte los códigos completos aquí. Además, no dude en seguirnos en Twitter y no olvide unirse a nuestro SubReddit de más de 120.000 ML y suscribirse a nuestro boletín. ¡Esperar! estas en telegrama? Ahora también puedes unirte a nosotros en Telegram.