Un marco agente de código abierto e independiente del modelo que admite la inyección de dependencias
Idealmente, puede evaluar aplicaciones agentes incluso mientras las está desarrollando, en lugar de que la evaluación sea una ocurrencia tardía. Sin embargo, para que esto funcione, debe poder burlarse de las dependencias internas y externas del agente que está desarrollando. Estoy muy entusiasmado con PydanticAI porque admite la inyección de dependencias desde cero. Es el primer marco que me ha permitido crear aplicaciones agentes basadas en evaluación.
En este artículo, hablaré sobre los desafíos principales y demostraré el desarrollo de un agente simple basado en evaluación utilizando PydanticAI.
Desafíos al desarrollar aplicaciones GenAI
Como muchos desarrolladores de GenAI, he estado esperando un marco agente que admita el ciclo de vida completo del desarrollo. Cada vez que aparece un nuevo marco, lo pruebo con la esperanza de que este sea el indicado; consulte, por ejemplo, mis artículos sobre DSPy, Langchain, LangGraph y Autogen.
Encuentro que existen desafíos fundamentales que enfrenta un desarrollador de software cuando desarrolla una aplicación basada en LLM. Por lo general, estos desafíos no son un obstáculo si está creando un PoC simple con GenAI, pero lo afectarán si está creando aplicaciones basadas en LLM en producción.
¿Qué desafíos?
(1) No determinismo: A diferencia de la mayoría de las API de software, las llamadas a un LLM con exactamente la misma entrada pueden devolver resultados diferentes cada vez. ¿Cómo se puede siquiera empezar a probar una aplicación de este tipo?
(2) Limitaciones del LLM: Los modelos fundamentales como GPT-4, Claude y Gemini están limitados por sus datos de entrenamiento (por ejemplo, sin acceso a información confidencial de la empresa), capacidad (por ejemplo, no se pueden invocar API y bases de datos empresariales) y no pueden planificar/razonar.
(3) Flexibilidad del LLM: Incluso si decide seguir los LLM de un solo proveedor como Anthropic, es posible que necesite un LLM diferente para cada paso; tal vez un paso de su flujo de trabajo necesite un modelo de lenguaje pequeño de baja latencia (Haiku), otro requiera gran capacidad de generación de código (Sonnet), y un tercer paso requiere una excelente conciencia contextual (Opus).
(4) Tasa de cambio: Las tecnologías GenAI avanzan rápidamente. Recientemente, muchas de las mejoras se han producido en las capacidades fundamentales del modelo. Los modelos fundamentales ya no se limitan a generar texto en función de las indicaciones del usuario. Ahora son multimodales, pueden generar resultados estructurados y pueden tener memoria. Sin embargo, si intenta crear de forma independiente del LLM, a menudo perderá el acceso a la API de bajo nivel que activará estas funciones.
Para ayudar a abordar el primer problema, el no determinismo, las pruebas de software deben incorporar un marco de evaluación. Nunca tendrás un software que funcione al 100%; en su lugar, deberá poder diseñar en torno a software que sea x% correcto, crear barreras de seguridad y supervisión humana para detectar las excepciones y monitorear el sistema en tiempo real para detectar regresiones. La clave de esta capacidad es desarrollo impulsado por la evaluación (mi término), una extensión del desarrollo de software basado en pruebas.
La solución alternativa actual para todas las limitaciones de LLM en el Desafío n.° 2 es utilizar arquitecturas agentes como RAG, proporciona al LLM acceso a herramientas y emplea patrones como Reflexión, ReACT y Cadena de pensamiento. Por lo tanto, su marco deberá tener la capacidad de orquestar agentes. Sin embargo, es difícil evaluar agentes que puedan recurrir a herramientas externas. Necesitas poder inyectar servidores proxy para estas dependencias externas para que pueda probarlos individualmente y evaluarlos a medida que construye.
Para afrontar el desafío número 3, un agente debe poder invocar las capacidades de diferentes tipos de modelos fundamentales. El marco de su agente debe ser LLM-agnóstico en la granularidad de un solo paso de un flujo de trabajo agente. Para abordar la consideración de la tasa de cambio (desafío n.° 4), desea conservar la capacidad de realizar acceso de bajo nivel a las API del modelo fundamental y a eliminar secciones de su código base que ya no son necesarias.
¿Existe un marco que cumpla con todos estos criterios? Durante mucho tiempo, la respuesta fue no. Lo más cerca que pude estar fue usar Langchain, la inyección de dependencia de pytest y profundizar con algo como esto (el ejemplo completo es aquí):
from unittest.mock import patch, Mock
from deepeval.metrics import GEvalllm_as_judge = GEval(
name="Correctness",
criteria="Determine whether the actual output is factually correct based on the expected output.",
evaluation_params=(LLMTestCaseParams.INPUT, LLMTestCaseParams.ACTUAL_OUTPUT),
model='gpt-3.5-turbo'
)
@patch('lg_weather_agent.retrieve_weather_data', Mock(return_value=chicago_weather))
def eval_query_rain_today():
input_query = "Is it raining in Chicago?"
expected_output = "No, it is not raining in Chicago right now."
result = lg_weather_agent.run_query(app, input_query)
actual_output = result(-1)
print(f"Actual: {actual_output} Expected: {expected_output}")
test_case = LLMTestCase(
input=input_query,
actual_output=actual_output,
expected_output=expected_output
)
llm_as_judge.measure(test_case)
print(llm_as_judge.score)
Esencialmente, construiría un objeto simulado (chicago_weather en el ejemplo anterior) para cada llamada de LLM y parchearía la llamada al LLM (retrieve_weather_data en el ejemplo anterior) con el objeto codificado cada vez que necesitara simular esa parte del flujo de trabajo agente. La inyección de dependencia está por todas partes, necesita un montón de objetos codificados y el flujo de trabajo de llamadas se vuelve extremadamente difícil de seguir. Tenga en cuenta que si no tiene inyección de dependencia, no hay manera de probar una función como esta: obviamente, el servicio externo devolverá el clima actual y no hay manera de determinar cuál es la respuesta correcta para una pregunta como si o no está lloviendo ahora mismo.
Entonces… ¿existe un marco de agente que admita la inyección de dependencias, que sea Pythonic, que proporcione acceso de bajo nivel a los LLM, que sea independiente del modelo, que admita la creación de una evaluación a la vez y que sea fácil de usar y seguir?
Casi. <a target="_blank" class="af pq" href="https://ai.pydantic.dev/” rel=”noopener ugc nofollow” target=”_blank”>PydanticAI cumple con los primeros 3 requisitos; el cuarto (acceso LLM de bajo nivel) no es posible, pero el diseño no lo excluye. En el resto de este artículo, le mostraré cómo usarlo para desarrollar una aplicación agente basada en evaluación.
1. Su primera aplicación PydanticAI
Comencemos creando una aplicación PydanticAI sencilla. Esto utilizará un LLM para responder preguntas sobre montañas:
agent = llm_utils.agent()
question = "What is the tallest mountain in British Columbia?"
print(">> ", question)
answer = agent.run_sync(question)
print(answer.data)
En el código anterior, estoy creando un agente (le mostraré cómo en breve) y luego llamo a run_sync pasando el mensaje del usuario y obteniendo la respuesta del LLM. run_sync es una forma de hacer que el agente invoque el LLM y espere la respuesta. Otras formas son ejecutar la consulta de forma asincrónica o transmitir su respuesta. (código completo está aquí si quieres seguirlo).
Ejecute el código anterior y obtendrá algo como:
>> What is the tallest mountain in British Columbia?
The tallest mountain in British Columbia is **Mount Robson**, at 3,954 metres (12,972 feet).
Para crear el agente, cree un modelo y luego dígale al agente que use ese modelo para todos sus pasos.
import pydantic_ai
from pydantic_ai.models.gemini import GeminiModeldef default_model() -> pydantic_ai.models.Model:
model = GeminiModel('gemini-1.5-flash', api_key=os.getenv('GOOGLE_API_KEY'))
return model
def agent() -> pydantic_ai.Agent:
return pydantic_ai.Agent(default_model())
La idea detrás de default_model() es utilizar un modelo relativamente económico pero rápido como Gemini Flash como predeterminado. Luego puede cambiar el modelo utilizado en pasos específicos según sea necesario pasando un modelo diferente a run_sync()
Soporte del modelo PydanticAI <a target="_blank" class="af pq" href="https://ai.pydantic.dev/api/models/base/#pydantic_ai.models” rel=”noopener ugc nofollow” target=”_blank”>parece escasopero los modelos más utilizados (los actuales de OpenAI, Groq, Gemini, Mistral, Ollama y Anthropic) son todos compatibles. A través de Ollama, puedes obtener acceso a Llama3, Starcoder2, Gemma2 y Phi3. No parece faltar nada significativo.
2. Pydantic con resultados estructurados
El ejemplo de la sección anterior arrojó texto en formato libre. En la mayoría de los flujos de trabajo agentes, querrá que el LLM devuelva datos estructurados para que pueda usarlos directamente en los programas.
Teniendo en cuenta que esta API es de Pydantic, devolver resultados estructurados es bastante sencillo. Simplemente defina la salida deseada como una clase de datos (el código completo está aquí):
from dataclasses import dataclass@dataclass
class Mountain:
name: str
location: str
height: float
Cuando cree el Agente, indíquele el tipo de salida deseado:
agent = Agent(llm_utils.default_model(),
result_type=Mountain,
system_prompt=(
"You are a mountaineering guide, who provides accurate information to the general public.",
"Provide all distances and heights in meters",
"Provide location as distance and direction from nearest big city",
))
Tenga en cuenta también el uso del indicador del sistema para especificar unidades, etc.
Al ejecutar esto en tres preguntas, obtenemos:
>> Tell me about the tallest mountain in British Columbia?
Mountain(name='Mount Robson', location='130km North of Vancouver', height=3999.0)
>> Is Mt. Hood easy to climb?
Mountain(name='Mt. Hood', location='60 km east of Portland', height=3429.0)
>> What's the tallest peak in the Enchantments?
Mountain(name='Mount Stuart', location='100 km east of Seattle', height=3000.0)
¿Pero qué tan bueno es este agente? ¿Es correcta la altura del monte Robson? ¿Es el monte Stuart realmente el pico más alto de los Encantamientos? ¡Toda esta información podría haber sido alucinada!
No hay forma de saber qué tan buena es una aplicación de agente a menos que evalúe al agente con respecto a las respuestas de referencia. No puedes simplemente “observarlo”. Desafortunadamente, aquí es donde muchos marcos de LLM se quedan cortos: hacen que sea muy difícil evaluarlos a medida que se desarrolla la aplicación de LLM.
3. Evalúe con respecto a las respuestas de referencia.
Es cuando comienza a evaluar las respuestas de referencia que PydanticAI comienza a mostrar sus fortalezas. Todo es bastante Pythonic, por lo que puedes crear métricas de evaluación personalizadas de forma muy sencilla.
Por ejemplo, así es como evaluaremos un objeto Mountain devuelto según tres criterios y crearemos una puntuación compuesta (código completo está aquí):
def evaluate(answer: Mountain, reference_answer: Mountain) -> Tuple(float, str):
score = 0
reason = ()
if reference_answer.name in answer.name:
score += 0.5
reason.append("Correct mountain identified")
if reference_answer.location in answer.location:
score += 0.25
reason.append("Correct city identified")
height_error = abs(reference_answer.height - answer.height)
if height_error < 10:
score += 0.25 * (10 - height_error)/10.0
reason.append(f"Height was {height_error}m off. Correct answer is {reference_answer.height}")
else:
reason.append(f"Wrong mountain identified. Correct answer is {reference_answer.name}")return score, ';'.join(reason)
Ahora podemos ejecutar esto en un conjunto de datos de preguntas y respuestas de referencia:
questions = (
"Tell me about the tallest mountain in British Columbia?",
"Is Mt. Hood easy to climb?",
"What's the tallest peak in the Enchantments?"
)reference_answers = (
Mountain("Robson", "Vancouver", 3954),
Mountain("Hood", "Portland", 3429),
Mountain("Dragontail", "Seattle", 2690)
)
total_score = 0
for l_question, l_reference_answer in zip(questions, reference_answers):
print(">> ", l_question)
l_answer = agent.run_sync(l_question)
print(l_answer.data)
l_score, l_reason = evaluate(l_answer.data, l_reference_answer)
print(l_score, ":", l_reason)
total_score += l_score
avg_score = total_score / len(questions)
Al ejecutar esto, obtenemos:
>> Tell me about the tallest mountain in British Columbia?
Mountain(name='Mount Robson', location='130 km North-East of Vancouver', height=3999.0)
0.75 : Correct mountain identified;Correct city identified;Height was 45.0m off. Correct answer is 3954
>> Is Mt. Hood easy to climb?
Mountain(name='Mt. Hood', location='60 km east of Portland, OR', height=3429.0)
1.0 : Correct mountain identified;Correct city identified;Height was 0.0m off. Correct answer is 3429
>> What's the tallest peak in the Enchantments?
Mountain(name='Dragontail Peak', location='14 km east of Leavenworth, WA', height=3008.0)
0.5 : Correct mountain identified;Height was 318.0m off. Correct answer is 2690
Average score: 0.75
La altura del monte Robson está a 45 m de distancia; La altura del pico Dragontail estaba a 318 m de distancia. ¿Cómo solucionarías esto?
Así es. Utilizaría una arquitectura RAG o equiparía al agente con una herramienta que proporcione la información de altura correcta. Usemos el último enfoque y veamos cómo hacerlo con Pydantic.
Observe cómo el desarrollo impulsado por la evaluación nos muestra el camino a seguir para mejorar nuestra aplicación agente.
4a. usando una herramienta
PydanticAI admite varias formas de proporcionar herramientas a un agente. Aquí, anoto una función que se llamará siempre que necesite la altura de una montaña (código completo aquí):
agent = Agent(llm_utils.default_model(),
result_type=Mountain,
system_prompt=(
"You are a mountaineering guide, who provides accurate information to the general public.",
"Use the provided tool to look up the elevation of many mountains."
"Provide all distances and heights in meters",
"Provide location as distance and direction from nearest big city",
))
@agent.tool
def get_height_of_mountain(ctx: RunContext(Tools), mountain_name: str) -> str:
return ctx.deps.elev_wiki.snippet(mountain_name)
La función, sin embargo, hace algo extraño. Extrae un objeto llamado elev_wiki del contexto de tiempo de ejecución del agente. Este objeto se pasa cuando llamamos a run_sync:
class Tools:
elev_wiki: wikipedia_tool.WikipediaContent
def __init__(self):
self.elev_wiki = OnlineWikipediaContent("List of mountains by elevation")tools = Tools() # Tools or FakeTools
l_answer = agent.run_sync(l_question, deps=tools) # note how we are able to inject
Debido a que el contexto de tiempo de ejecución se puede pasar a cada invocación de agente o llamada de herramienta, podemos usarlo para realizar inyección de dependencia en PydanticAI. Verás esto en la siguiente sección.
La propia wiki simplemente consulta Wikipedia en línea (código aquí) y extrae el contenido de la página y pasa la información de montaña apropiada al agente:
import wikipediaclass OnlineWikipediaContent(WikipediaContent):
def __init__(self, topic: str):
print(f"Will query online Wikipedia for information on {topic}")
self.page = wikipedia.page(topic)
def url(self) -> str:
return self.page.url
def html(self) -> str:
return self.page.html()
De hecho, cuando lo ejecutamos, ahora obtenemos las alturas correctas:
Will query online Wikipedia for information on List of mountains by elevation
>> Tell me about the tallest mountain in British Columbia?
Mountain(name='Mount Robson', location='100 km west of Jasper', height=3954.0)
0.75 : Correct mountain identified;Height was 0.0m off. Correct answer is 3954
>> Is Mt. Hood easy to climb?
Mountain(name='Mt. Hood', location='50 km ESE of Portland, OR', height=3429.0)
1.0 : Correct mountain identified;Correct city identified;Height was 0.0m off. Correct answer is 3429
>> What's the tallest peak in the Enchantments?
Mountain(name='Mount Stuart', location='Cascades, Washington, US', height=2869.0)
0 : Wrong mountain identified. Correct answer is Dragontail
Average score: 0.58
4b. Dependencia inyectando un servicio simulado
Esperar la llamada API a Wikipedia cada vez durante el desarrollo o las pruebas es una mala idea. En lugar de eso, querremos burlarnos de la respuesta de Wikipedia para que podamos desarrollarnos rápidamente y tener la garantía del resultado que vamos a obtener.
Hacer eso es muy simple. Creamos una contraparte falsa del servicio Wikipedia:
class FakeWikipediaContent(WikipediaContent):
def __init__(self, topic: str):
if topic == "List of mountains by elevation":
print(f"Will used cached Wikipedia information on {topic}")
self.url_ = "https://en.wikipedia.org/wiki/List_of_mountains_by_elevation"
with open("mountains.html", "rb") as ifp:
self.html_ = ifp.read().decode("utf-8")def url(self) -> str:
return self.url_
def html(self) -> str:
return self.html_
Luego, inyecta este objeto falso en el contexto de ejecución del agente durante el desarrollo:
class FakeTools:
elev_wiki: wikipedia_tool.WikipediaContent
def __init__(self):
self.elev_wiki = FakeWikipediaContent("List of mountains by elevation")tools = FakeTools() # Tools or FakeTools
l_answer = agent.run_sync(l_question, deps=tools) # note how we are able to inject
Esta vez, cuando ejecutamos, la evaluación utiliza el contenido de Wikipedia almacenado en caché:
Will used cached Wikipedia information on List of mountains by elevation
>> Tell me about the tallest mountain in British Columbia?
Mountain(name='Mount Robson', location='100 km west of Jasper', height=3954.0)
0.75 : Correct mountain identified;Height was 0.0m off. Correct answer is 3954
>> Is Mt. Hood easy to climb?
Mountain(name='Mt. Hood', location='50 km ESE of Portland, OR', height=3429.0)
1.0 : Correct mountain identified;Correct city identified;Height was 0.0m off. Correct answer is 3429
>> What's the tallest peak in the Enchantments?
Mountain(name='Mount Stuart', location='Cascades, Washington, US', height=2869.0)
0 : Wrong mountain identified. Correct answer is Dragontail
Average score: 0.58
Mire detenidamente el resultado anterior: hay diferentes errores del ejemplo de disparo cero. En la Sección #2, el LLM eligió a Vancouver como la ciudad más cercana al Monte Robson y Dragontail como el pico más alto de los Encantamientos. Esas respuestas resultaron ser correctas. Ahora elige a Jasper y Mt. Stuart. Necesitamos trabajar más para corregir estos errores, pero el desarrollo impulsado por la evaluación al menos nos da una dirección a seguir.
Limitaciones actuales
PydanticAI es muy nuevo. Hay un par de puntos donde se podría mejorar:
- No hay acceso de bajo nivel al modelo en sí. Por ejemplo, diferentes modelos fundamentales admiten el almacenamiento en caché de contexto, el almacenamiento en caché de avisos, etc. La abstracción del modelo en PydanticAI no proporciona una forma de configurarlos en el modelo. Idealmente, podemos encontrar una forma kwargs de realizar dichas configuraciones.
- La necesidad de crear dos versiones de dependencias de agentes, una real y otra falsa, es bastante común. Sería bueno si pudiéramos anotar una herramienta o proporcionar una forma sencilla de cambiar entre los dos tipos de servicios en todos los ámbitos.
- Durante el desarrollo, no es necesario iniciar sesión tanto. Pero cuando vaya a ejecutar el agente, normalmente querrá registrar las indicaciones y respuestas. A veces querrás registrar las respuestas intermedias. La forma de hacerlo parece ser un producto comercial llamado Logfire. Lo ideal sería un marco de registro OSS independiente de la nube que se integre con la biblioteca PydanticAI.
Es posible que ya existan y los haya perdido, o tal vez ya se hayan implementado cuando leas este artículo. En cualquier caso, deja un comentario para futuros lectores.
En general, me gusta PydanticAI: ofrece una forma muy limpia y pitónica de crear aplicaciones agentes basadas en evaluaciones.
Próximos pasos sugeridos:
- Esta es una de esas publicaciones de blog en las que se beneficiará al ejecutar los ejemplos porque describe un proceso de desarrollo así como una nueva biblioteca. Este repositorio de GitHub contiene el ejemplo de PydanticAI que analicé en esta publicación: https://github.com/lakshmanok/lakblogs/tree/main/pydantic_ai_mountains Siga las instrucciones del archivo README para probarlo.
- Documentación de Pydantic ai: <a target="_blank" class="af pq" href="https://ai.pydantic.dev/” rel=”noopener ugc nofollow” target=”_blank”>https://ai.pydantic.dev/
- Parchear un flujo de trabajo de Langchain con objetos simulados. Mi solución “antes”: https://github.com/lakshmanok/lakblogs/blob/main/genai_agents/eval_weather_agent.py